From a3db48a840bf0e793bbf0630eb65d4d4dec04378 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 30 Mar 2026 10:45:30 -0500 Subject: [PATCH 1/4] 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 7daaf109cbdd..afb163c8a675 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -166,6 +166,11 @@ CoinControlDialog::~CoinControlDialog() delete ui; } +void CoinControlDialog::refreshLabels() +{ + CoinControlDialog::updateLabels(m_coin_control, model, this); +} + // ok button void CoinControlDialog::buttonBoxClicked(QAbstractButton* button) { @@ -650,6 +655,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 ae1fcb0797fd710791fd0de5d2d4c2504c757b78 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 30 Mar 2026 23:27:27 -0500 Subject: [PATCH 2/4] 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 6b5a5553bf6fb3e60acb734ec86d3c2def06166c Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 2 Apr 2026 10:51:25 -0500 Subject: [PATCH 3/4] fix(qt): gate coin control shift-select to checkbox clicks (CodeRabbit) --- src/qt/coincontroltreewidget.cpp | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/qt/coincontroltreewidget.cpp b/src/qt/coincontroltreewidget.cpp index 3d0f2b8c8ae4..b909a59100cf 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; } +static bool isCheckboxClick(QTreeWidget* tree, QTreeWidgetItem* item, const QPoint& pos) +{ + int COLUMN_CHECKBOX = 0; + + if (!tree || !item || tree->columnAt(pos.x()) != COLUMN_CHECKBOX) { + return false; + } + + const QModelIndex index = tree->indexFromItem(item, COLUMN_CHECKBOX); + if (!index.isValid()) { + return false; + } + + QStyleOptionViewItem option; + option.initFrom(tree); + option.rect = tree->visualRect(index); + option.features = QStyleOptionViewItem::HasCheckIndicator; + option.checkState = item->checkState(COLUMN_CHECKBOX); + + return tree->style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &option, tree).contains(pos); +} + void CoinControlTreeWidget::mouseReleaseEvent(QMouseEvent *event) { int COLUMN_CHECKBOX = 0; QTreeWidgetItem* clickedItem = itemAt(event->pos()); + const bool isCheckboxInteraction = isCheckboxClick(this, 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; From fcb84c34345c76c1b6325f0e3bf3e13a69a03b23 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 29 May 2026 11:14:00 -0500 Subject: [PATCH 4/4] fix(qt): make isCheckboxClick a member to access protected indexFromItem QTreeWidget::indexFromItem is protected, so the free helper failed to compile under Qt 5. Move the helper into CoinControlTreeWidget so it inherits access; behavior (column, validity, and check-indicator hit test) is unchanged. --- src/qt/coincontroltreewidget.cpp | 14 +++++++------- src/qt/coincontroltreewidget.h | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/qt/coincontroltreewidget.cpp b/src/qt/coincontroltreewidget.cpp index b909a59100cf..db103f403f54 100644 --- a/src/qt/coincontroltreewidget.cpp +++ b/src/qt/coincontroltreewidget.cpp @@ -49,26 +49,26 @@ static bool isLeafItem(QTreeWidgetItem* item) return item && item->data(COLUMN_ADDRESS, Qt::UserRole).toString().length() == 64; } -static bool isCheckboxClick(QTreeWidget* tree, QTreeWidgetItem* item, const QPoint& pos) +bool CoinControlTreeWidget::isCheckboxClick(QTreeWidgetItem* item, const QPoint& pos) const { int COLUMN_CHECKBOX = 0; - if (!tree || !item || tree->columnAt(pos.x()) != COLUMN_CHECKBOX) { + if (!item || columnAt(pos.x()) != COLUMN_CHECKBOX) { return false; } - const QModelIndex index = tree->indexFromItem(item, COLUMN_CHECKBOX); + const QModelIndex index = indexFromItem(item, COLUMN_CHECKBOX); if (!index.isValid()) { return false; } QStyleOptionViewItem option; - option.initFrom(tree); - option.rect = tree->visualRect(index); + option.initFrom(this); + option.rect = visualRect(index); option.features = QStyleOptionViewItem::HasCheckIndicator; option.checkState = item->checkState(COLUMN_CHECKBOX); - return tree->style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &option, tree).contains(pos); + return style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &option, this).contains(pos); } void CoinControlTreeWidget::mouseReleaseEvent(QMouseEvent *event) @@ -76,7 +76,7 @@ void CoinControlTreeWidget::mouseReleaseEvent(QMouseEvent *event) int COLUMN_CHECKBOX = 0; QTreeWidgetItem* clickedItem = itemAt(event->pos()); - const bool isCheckboxInteraction = isCheckboxClick(this, clickedItem, event->pos()); + const bool isCheckboxInteraction = isCheckboxClick(clickedItem, event->pos()); bool isShiftClick = (event->button() == Qt::LeftButton) && (event->modifiers() & Qt::ShiftModifier) 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}; };