Files
wowlet/src/ReceiveWidget.cpp
T
jwinterm 252b358baa feat(scene): per-lap variety, the occasional face-plant, and a sky per tab
Every tab was running the identical eight seconds forever. Three changes:

- Each lap rolls its own timing: 3-5s of empty field, and the money now enters
  on its own delay at its own pace instead of shadowing the doge every time.
- Roughly one lap in six the doge eats it partway down the field: he tips
  nose-first onto the grass, GAME OVER comes up, and the coins he never reached
  are still sitting there. Costs no new art — it is the running sprite pivoted
  on its own feet.
- A `variant` on SceneWidget picks the sky: dusk (History, lock screen), dawn
  (Receive), night (Send, with stars and a moon instead of the sun).

The cycle is now a timer-driven state machine rather than one SequentialAnimation.
That is not stylistic: a lap that rolls its own durations has to assign them
somewhere, and assigning a duration to a child of a *running* SequentialAnimation
makes the group recompute and re-enter its own ScriptAction until the JS stack
blows. Qt flags the binding form of the same mistake as a binding loop.

Verified by headless qml6 render: a forced trip lap, and a clean run with no QML
warnings.
2026-08-31 13:23:41 -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);
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;