From 10a977425743a36d3f9beec0ceb2cda479219316 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 30 Mar 2026 10:45:30 -0500 Subject: [PATCH 1/3] feat(qt): add shift-click range selection to coin control dialog Override mouse events in CoinControlTreeWidget to support shift-click for selecting/deselecting a contiguous range of UTXOs. Tracks an anchor item from the last normal click, and on shift-click sets all leaf items between anchor and target to the anchor's check state. Uses the existing setEnabled(false) bulk-update pattern to suppress per-item label recalculation. --- src/qt/coincontroldialog.cpp | 6 +++ src/qt/coincontroldialog.h | 2 + src/qt/coincontroltreewidget.cpp | 82 ++++++++++++++++++++++++++++++++ src/qt/coincontroltreewidget.h | 8 +++- 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index 487693a498a3..652bd109fd39 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -165,6 +165,11 @@ CoinControlDialog::~CoinControlDialog() delete ui; } +void CoinControlDialog::refreshLabels() +{ + CoinControlDialog::updateLabels(m_coin_control, model, this); +} + // ok button void CoinControlDialog::buttonBoxClicked(QAbstractButton* button) { @@ -648,6 +653,7 @@ void CoinControlDialog::updateView() bool treeMode = ui->radioTreeMode->isChecked(); ui->treeWidget->clear(); + ui->treeWidget->resetAnchor(); ui->treeWidget->setEnabled(false); // performance, otherwise updateLabels would be called for every checked checkbox ui->treeWidget->setAlternatingRowColors(!treeMode); QFlags flgCheckbox = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index e59b833d8f4c..2079abdfbf5f 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -48,6 +48,8 @@ class CoinControlDialog : public QDialog // static because also called from sendcoinsdialog static void updateLabels(wallet::CCoinControl& m_coin_control, WalletModel*, QDialog*); + void refreshLabels(); + static QList payAmounts; static bool fSubtractFeeFromAmount; diff --git a/src/qt/coincontroltreewidget.cpp b/src/qt/coincontroltreewidget.cpp index cb23bdb553f1..3821b5d66eab 100644 --- a/src/qt/coincontroltreewidget.cpp +++ b/src/qt/coincontroltreewidget.cpp @@ -5,12 +5,19 @@ #include #include +#include + CoinControlTreeWidget::CoinControlTreeWidget(QWidget *parent) : QTreeWidget(parent) { } +void CoinControlTreeWidget::resetAnchor() +{ + m_lastClickedItem = nullptr; +} + void CoinControlTreeWidget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Space) // press spacebar -> select checkbox @@ -32,3 +39,78 @@ void CoinControlTreeWidget::keyPressEvent(QKeyEvent *event) this->QTreeWidget::keyPressEvent(event); } } + +// Helper: check if an item is a leaf node (UTXO) by its 64-char tx hash +static bool isLeafItem(QTreeWidgetItem* item) +{ + int COLUMN_ADDRESS = 3; + return item && item->data(COLUMN_ADDRESS, Qt::UserRole).toString().length() == 64; +} + +void CoinControlTreeWidget::mouseReleaseEvent(QMouseEvent *event) +{ + int COLUMN_CHECKBOX = 0; + + QTreeWidgetItem* clickedItem = itemAt(event->pos()); + + bool isShiftClick = (event->button() == Qt::LeftButton) + && (event->modifiers() & Qt::ShiftModifier) + && m_lastClickedItem + && clickedItem + && clickedItem != m_lastClickedItem + && isLeafItem(clickedItem); + + if (!isShiftClick) { + // Normal click — let Qt handle the checkbox toggle on release + QTreeWidget::mouseReleaseEvent(event); + // Record anchor after toggle so we capture the post-toggle state + if (clickedItem && isLeafItem(clickedItem)) { + m_lastClickedItem = clickedItem; + } + return; + } + + // Shift+click: select/deselect the range between anchor and target + // Read the anchor's current check state live (not cached) so it stays + // correct after bulk operations like Select All or parent tristate changes + Qt::CheckState stateToApply = m_lastClickedItem->checkState(COLUMN_CHECKBOX); + + // Collect visible leaf items in display order, skipping children of + // collapsed parent nodes so we don't toggle coins the user can't see + std::vector leafItems; + int anchorIdx = -1; + int targetIdx = -1; + QTreeWidgetItemIterator it(this); + while (*it) { + if (isLeafItem(*it)) { + QTreeWidgetItem* parent = (*it)->parent(); + if (!parent || parent->isExpanded()) { + if (*it == m_lastClickedItem) anchorIdx = leafItems.size(); + if (*it == clickedItem) targetIdx = leafItems.size(); + leafItems.push_back(*it); + } + } + ++it; + } + + if (anchorIdx < 0 || targetIdx < 0) { + QTreeWidget::mouseReleaseEvent(event); + return; + } + + if (anchorIdx > targetIdx) std::swap(anchorIdx, targetIdx); + + // Batch update: disable widget to suppress per-item updateLabels calls + setEnabled(false); + for (int i = anchorIdx; i <= targetIdx; ++i) { + QTreeWidgetItem* item = leafItems[i]; + if (!item->isDisabled() && item->checkState(COLUMN_CHECKBOX) != stateToApply) { + item->setCheckState(COLUMN_CHECKBOX, stateToApply); + } + } + setEnabled(true); + + // Single label update for the whole batch + CoinControlDialog* coinControlDialog = qobject_cast(this->parentWidget()); + if (coinControlDialog) coinControlDialog->refreshLabels(); +} diff --git a/src/qt/coincontroltreewidget.h b/src/qt/coincontroltreewidget.h index ac03a409c120..5b8b1b638236 100644 --- a/src/qt/coincontroltreewidget.h +++ b/src/qt/coincontroltreewidget.h @@ -6,6 +6,7 @@ #define BITCOIN_QT_COINCONTROLTREEWIDGET_H #include +#include #include class CoinControlTreeWidget : public QTreeWidget @@ -14,9 +15,14 @@ class CoinControlTreeWidget : public QTreeWidget public: explicit CoinControlTreeWidget(QWidget *parent = nullptr); + void resetAnchor(); protected: - virtual void keyPressEvent(QKeyEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + +private: + QTreeWidgetItem* m_lastClickedItem{nullptr}; }; #endif // BITCOIN_QT_COINCONTROLTREEWIDGET_H From 099bf3b3051a6d9f6683b2a084803685154fc2aa Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 30 Mar 2026 23:27:27 -0500 Subject: [PATCH 2/3] fix(qt): restrict coin control shift-select anchor to left-clicks on enabled items Only record the shift-click anchor on left-button clicks to enabled (non-locked) leaf items, matching standard UI behavior. Also guard the shift-click path so it falls back to normal click handling if either endpoint has become disabled since being recorded. --- src/qt/coincontroltreewidget.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/qt/coincontroltreewidget.cpp b/src/qt/coincontroltreewidget.cpp index 3821b5d66eab..3d0f2b8c8ae4 100644 --- a/src/qt/coincontroltreewidget.cpp +++ b/src/qt/coincontroltreewidget.cpp @@ -58,13 +58,16 @@ void CoinControlTreeWidget::mouseReleaseEvent(QMouseEvent *event) && m_lastClickedItem && clickedItem && clickedItem != m_lastClickedItem - && isLeafItem(clickedItem); + && isLeafItem(clickedItem) + && !clickedItem->isDisabled() + && !m_lastClickedItem->isDisabled(); if (!isShiftClick) { // Normal click — let Qt handle the checkbox toggle on release QTreeWidget::mouseReleaseEvent(event); // Record anchor after toggle so we capture the post-toggle state - if (clickedItem && isLeafItem(clickedItem)) { + if (event->button() == Qt::LeftButton && clickedItem + && isLeafItem(clickedItem) && !clickedItem->isDisabled()) { m_lastClickedItem = clickedItem; } return; From 708690519704cb57c957830b6242a9823b723447 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 2 Apr 2026 10:51:25 -0500 Subject: [PATCH 3/3] fix(qt): gate coin control shift-select to checkbox clicks (CodeRabbit) --- src/qt/coincontroltreewidget.cpp | 32 +++++++++++++++++++++++++++++++- src/qt/coincontroltreewidget.h | 2 ++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/qt/coincontroltreewidget.cpp b/src/qt/coincontroltreewidget.cpp index 3d0f2b8c8ae4..db103f403f54 100644 --- a/src/qt/coincontroltreewidget.cpp +++ b/src/qt/coincontroltreewidget.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include #include CoinControlTreeWidget::CoinControlTreeWidget(QWidget *parent) : @@ -47,27 +49,55 @@ static bool isLeafItem(QTreeWidgetItem* item) return item && item->data(COLUMN_ADDRESS, Qt::UserRole).toString().length() == 64; } +bool CoinControlTreeWidget::isCheckboxClick(QTreeWidgetItem* item, const QPoint& pos) const +{ + int COLUMN_CHECKBOX = 0; + + if (!item || columnAt(pos.x()) != COLUMN_CHECKBOX) { + return false; + } + + const QModelIndex index = indexFromItem(item, COLUMN_CHECKBOX); + if (!index.isValid()) { + return false; + } + + QStyleOptionViewItem option; + option.initFrom(this); + option.rect = visualRect(index); + option.features = QStyleOptionViewItem::HasCheckIndicator; + option.checkState = item->checkState(COLUMN_CHECKBOX); + + return style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &option, this).contains(pos); +} + void CoinControlTreeWidget::mouseReleaseEvent(QMouseEvent *event) { int COLUMN_CHECKBOX = 0; QTreeWidgetItem* clickedItem = itemAt(event->pos()); + const bool isCheckboxInteraction = isCheckboxClick(clickedItem, event->pos()); bool isShiftClick = (event->button() == Qt::LeftButton) && (event->modifiers() & Qt::ShiftModifier) && m_lastClickedItem && clickedItem && clickedItem != m_lastClickedItem + && isCheckboxInteraction && isLeafItem(clickedItem) && !clickedItem->isDisabled() && !m_lastClickedItem->isDisabled(); if (!isShiftClick) { // Normal click — let Qt handle the checkbox toggle on release + const Qt::CheckState previousState = clickedItem ? clickedItem->checkState(COLUMN_CHECKBOX) : Qt::Unchecked; QTreeWidget::mouseReleaseEvent(event); // Record anchor after toggle so we capture the post-toggle state if (event->button() == Qt::LeftButton && clickedItem - && isLeafItem(clickedItem) && !clickedItem->isDisabled()) { + && isCheckboxInteraction + && isLeafItem(clickedItem) + && !clickedItem->isDisabled() + && clickedItem->checkState(COLUMN_CHECKBOX) != previousState) { m_lastClickedItem = clickedItem; } return; diff --git a/src/qt/coincontroltreewidget.h b/src/qt/coincontroltreewidget.h index 5b8b1b638236..3ed27b6df285 100644 --- a/src/qt/coincontroltreewidget.h +++ b/src/qt/coincontroltreewidget.h @@ -22,6 +22,8 @@ class CoinControlTreeWidget : public QTreeWidget void mouseReleaseEvent(QMouseEvent *event) override; private: + bool isCheckboxClick(QTreeWidgetItem* item, const QPoint& pos) const; + QTreeWidgetItem* m_lastClickedItem{nullptr}; };