Files
wowlet/src/ReceiveWidget.cpp
T
jwinterm f1144c483e fix(scene): the wipeout is a skid, the win says so, and each tab gets its own amount of scene
The crash launched him. Rotating 66 degrees about a pivot inside the
sprite, shearing him through the middle and scaling him about the same
point left him diagonal in mid-air, elongated, nowhere near the ground —
a stunt, not a trip.

It is now a face-plant and a skid: 26 degrees, a 21px drop so the snout
reaches the grass, and 26px of forward slide along the ground with the
dust coming up. The shear is gone entirely. The pivot is the snout at the
grass line rather than a point inside the body.

The other half of looking airborne was the frozen frame: stopping a run
cycle leaves whatever mid-stride pose the gif was on, legs extended. He
now lands on frame 4, the one with the legs tucked under him.

Clearing the field and getting across now says WIN, with the tally, the
way eating it says GAME OVER. Before, a won lap just ended and the next
one opened with a doge sitting down, which reads as nothing having
happened.

And the scene comes in three amounts, because a form should not have to
compete with a dog: History runs the whole thing, Receive is a doge
sitting in the field, Send is money blowing past and nothing else. One
QML file, gated by mode; the beat machine only runs where there is a run.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-03 13:07:04 -04:00

289 lines
9.7 KiB
C++

// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: The Monero Project
#include "ReceiveWidget.h"
#include "ui_ReceiveWidget.h"
#include <QMenu>
#include "libwalletqt/rows/SubaddressRow.h"
#include "dialog/PaymentRequestDialog.h"
#include "dialog/QrCodeDialog.h"
#include "utils/config.h"
#include "utils/Icons.h"
#include "utils/Utils.h"
#include "widgets/SceneWidget.h"
ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
: QWidget(parent)
, ui(new Ui::ReceiveWidget)
, m_wallet(wallet)
{
ui->setupUi(this);
// wowlet: ambient scene as a short banner under the address table. A layout
// item, never an overlay over the table.
auto *scene = new SceneWidget(this, SceneWidget::Dawn, SceneWidget::Sit);
scene->setMinimumHeight(110);
scene->setMaximumHeight(130);
ui->verticalLayout->addWidget(scene);
m_model = m_wallet->subaddressModel();
m_proxyModel = new SubaddressProxyModel(this, m_wallet->subaddress());
m_proxyModel->setSourceModel(m_model);
ui->addresses->setModel(m_proxyModel);
ui->addresses->setColumnHidden(SubaddressModel::isUsed, true);
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Index, QHeaderView::ResizeToContents);
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Address, QHeaderView::ResizeToContents);
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Label, QHeaderView::Stretch);
connect(ui->addresses->selectionModel(), &QItemSelectionModel::selectionChanged, [=](const QItemSelection &selected, const QItemSelection &deselected){
this->updateQrCode();
});
connect(m_model, &SubaddressModel::modelReset, [this](){
this->updateQrCode();
});
ui->qrCode->setCursor(Qt::PointingHandCursor);
// header context menu
ui->addresses->header()->setContextMenuPolicy(Qt::CustomContextMenu);
m_headerMenu = new QMenu(this);
auto subMenu = new QMenu(this);
subMenu->setTitle("Columns");
this->addOption(m_headerMenu, "Show used addresses", Config::showUsedAddresses, [this](bool show){
m_proxyModel->invalidate();
});
this->addOption(m_headerMenu, "Show hidden addresses", Config::showHiddenAddresses, [this](bool show){
m_proxyModel->invalidate();
});
this->addOption(m_headerMenu, "Show full addresses", Config::showFullAddresses, [this](bool show){
m_proxyModel->invalidate();
});
this->addOption(m_headerMenu, "Show change address", Config::showChangeAddresses, [this](bool show){
m_proxyModel->invalidate();
});
m_headerMenu->addMenu(subMenu);
this->addOption(subMenu, "Show index", Config::showAddressIndex, [this](bool show){
ui->addresses->setColumnHidden(0, !show);
});
this->addOption(subMenu, "Show labels", Config::showAddressLabels, [this](bool show){
ui->addresses->setColumnHidden(2, !show);
});
connect(ui->addresses->header(), &QHeaderView::customContextMenuRequested, this, &ReceiveWidget::showHeaderMenu);
ui->toolBtn_options->setMenu(m_headerMenu);
// context menu
ui->addresses->setContextMenuPolicy(Qt::CustomContextMenu);
m_showTransactionsAction = new QAction("Show transactions", this);
connect(m_showTransactionsAction, &QAction::triggered, this, &ReceiveWidget::onShowTransactions);
connect(ui->addresses, &QTreeView::customContextMenuRequested, this, &ReceiveWidget::showContextMenu);
connect(ui->addresses, &SubaddressView::copyAddress, this, &ReceiveWidget::copyAddress);
connect(ui->qrCode, &ClickableLabel::clicked, this, &ReceiveWidget::showQrCodeDialog);
connect(ui->search, &QLineEdit::textChanged, this, &ReceiveWidget::setSearchFilter);
connect(ui->btn_generateSubaddress, &QPushButton::clicked, this, &ReceiveWidget::generateSubaddress);
connect(ui->btn_createPaymentRequest, &QPushButton::clicked, this, &ReceiveWidget::createPaymentRequest);
}
void ReceiveWidget::addOption(QMenu *menu, const QString &text, Config::ConfigKey key, const std::function<void(bool show)>& func) {
// QMenu takes ownership of the returned QAction.
QAction *action = menu->addAction(text, func);
action->setCheckable(true);
bool toggled = conf()->get(key).toBool();
action->setChecked(toggled);
func(toggled);
connect(action, &QAction::toggled, [key](bool toggled){
conf()->set(key, toggled);
});
}
void ReceiveWidget::setSearchbarVisible(bool visible) {
ui->frame_search->setVisible(visible);
}
void ReceiveWidget::focusSearchbar() {
ui->search->setFocusPolicy(Qt::StrongFocus);
ui->search->setFocus();
}
QString ReceiveWidget::getAddress(quint32 minorIndex) {
bool ok;
QString reason;
QString address = m_wallet->getAddressSafe(m_wallet->currentSubaddressAccount(), minorIndex, ok, reason);
if (!ok) {
Utils::showError(this, "Unable to get address",
QString("Reason: %1\n\n"
"WARNING!\n\n"
"Potential wallet file corruption detected.\n\n"
"To prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\n"
"Restore your wallet from seed, keys, or device.\n\n"
"Please report this incident to the Feather developers.\n\n"
"WARNING!").arg(reason), {}, "report_an_issue");
return {};
}
return address;
}
void ReceiveWidget::copyAddress() {
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
return;
}
QString address = this->getAddress(index.row());
Utils::copyToClipboard(address);
}
void ReceiveWidget::copyLabel() {
QModelIndex index = ui->addresses->currentIndex();
Utils::copyColumn(&index, SubaddressModel::Label);
}
void ReceiveWidget::editLabel() {
QModelIndex index = ui->addresses->currentIndex().siblingAtColumn(SubaddressModel::ModelColumn::Label);
ui->addresses->setCurrentIndex(index);
ui->addresses->edit(index);
}
void ReceiveWidget::showContextMenu(const QPoint &point) {
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
return;
}
auto& row = m_model->entryFromIndex(index);
auto *menu = new QMenu(ui->addresses);
menu->addAction("Copy address", this, &ReceiveWidget::copyAddress);
menu->addAction("Copy label", this, &ReceiveWidget::copyLabel);
menu->addAction("Edit label", this, &ReceiveWidget::editLabel);
if (row.used) {
menu->addAction(m_showTransactionsAction);
}
QAction *actionPin = menu->addAction("Pin address", [this](bool toggled){
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
return;
}
auto& row = m_model->entryFromIndex(index);
m_wallet->subaddress()->setPinned(row.address, toggled);
m_proxyModel->invalidate();
});
actionPin->setCheckable(true);
actionPin->setChecked(row.pinned);
QAction *actionHide = menu->addAction("Hide address", [this](bool toggled){
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
return;
}
auto& row = m_model->entryFromIndex(index);
m_wallet->subaddress()->setHidden(row.address, toggled);
m_proxyModel->invalidate();
});
actionHide->setCheckable(true);
actionHide->setChecked(row.hidden);
if (m_wallet->isHwBacked()) {
menu->addAction("Show on device", this, &ReceiveWidget::showOnDevice);
}
menu->popup(ui->addresses->viewport()->mapToGlobal(point));
}
void ReceiveWidget::createPaymentRequest() {
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
return;
}
QString address = this->getAddress(index.row());
PaymentRequestDialog dialog{this, m_wallet, address};
dialog.exec();
}
void ReceiveWidget::onShowTransactions() {
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
return;
}
QString address = this->getAddress(index.row());
emit showTransactions(address);
}
void ReceiveWidget::setSearchFilter(const QString &filter) {
m_proxyModel->setSearchFilter(filter);
}
void ReceiveWidget::showHeaderMenu(const QPoint& position)
{
Q_UNUSED(position)
m_headerMenu->exec(QCursor::pos());
}
void ReceiveWidget::showOnDevice() {
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
return;
}
m_wallet->deviceShowAddressAsync(m_wallet->currentSubaddressAccount(), index.row(), "");
}
void ReceiveWidget::generateSubaddress() {
bool r = m_wallet->subaddress()->addRow("");
if (!r) {
Utils::showError(this, "Failed to generate subaddress", m_wallet->subaddress()->getError());
}
}
void ReceiveWidget::updateQrCode(){
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
ui->qrCode->clear();
ui->btn_createPaymentRequest->hide();
return;
}
QString address = this->getAddress(index.row());
const QrCode qrc(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::MEDIUM);
int width = ui->qrCode->width() - 4;
if (qrc.isValid()) {
ui->qrCode->setPixmap(qrc.toPixmap(1).scaled(width, width, Qt::KeepAspectRatio));
ui->btn_createPaymentRequest->show();
}
}
void ReceiveWidget::showQrCodeDialog() {
QModelIndex index = getCurrentIndex();
if (!index.isValid()) {
return;
}
QString address = this->getAddress(index.row());
QrCode qr(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH);
QrCodeDialog dialog{this, &qr, "Address"};
dialog.exec();
}
QModelIndex ReceiveWidget::getCurrentIndex()
{
return m_proxyModel->mapToSource(ui->addresses->currentIndex());
}
ReceiveWidget::~ReceiveWidget() = default;