From a3f8c494e324fd1781fcfeec5bd1e8ad3ab1b31c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 29 Jun 2026 11:44:31 -0500 Subject: [PATCH] Ignore stationary pointer hover in clipboard --- shell/plugins/clipboard/Clipboard.qml | 38 +++++++++++++++++++++++---- test/shell.d/clipboard-test.sh | 23 ++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/shell/plugins/clipboard/Clipboard.qml b/shell/plugins/clipboard/Clipboard.qml index e3c9e3f2..eb9660b9 100644 --- a/shell/plugins/clipboard/Clipboard.qml +++ b/shell/plugins/clipboard/Clipboard.qml @@ -14,6 +14,8 @@ Item { property string filterText: "" property int selectedIndex: 0 property bool cursorActive: false + property real lastPointerX: -1 + property real lastPointerY: -1 property bool clearConfirmOpen: false property var history: [] @@ -45,6 +47,7 @@ Item { root.filterText = "" root.selectedIndex = 0 root.cursorActive = true + root.disarmPointer() root.rebuildDisplay() Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } @@ -97,6 +100,7 @@ Item { function cancelClearHistory() { root.clearConfirmOpen = false + root.disarmPointer() Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } @@ -105,6 +109,7 @@ Item { root.saveHistory() root.selectedIndex = 0 root.cursorActive = false + root.disarmPointer() root.clearConfirmOpen = false root.rebuildDisplay() Qt.callLater(function() { keyCatcher.forceActiveFocus() }) @@ -124,6 +129,7 @@ Item { root.selectedIndex = displayModel.count - 2 } + root.disarmPointer() root.rebuildDisplay() } @@ -155,6 +161,7 @@ Item { function select(delta) { if (displayModel.count === 0) return + root.disarmPointer() if (!cursorActive) { cursorActive = true selectedIndex = delta < 0 ? displayModel.count - 1 : 0 @@ -168,9 +175,30 @@ Item { root.filterText = nextFilter root.selectedIndex = 0 root.cursorActive = true + root.disarmPointer() root.rebuildDisplay() } + function disarmPointer() { + root.lastPointerX = -1 + root.lastPointerY = -1 + } + + function pointerMovedInCard(item, mouse) { + var point = item.mapToItem(card, mouse.x, mouse.y) + var moved = root.lastPointerX >= 0 + && (Math.abs(point.x - root.lastPointerX) > 1 || Math.abs(point.y - root.lastPointerY) > 1) + root.lastPointerX = point.x + root.lastPointerY = point.y + return moved + } + + function selectFromPointer(index, item, mouse) { + if (!root.pointerMovedInCard(item, mouse)) return + root.cursorActive = true + root.selectedIndex = index + } + function activateIndex(index) { if (index < 0 || index >= displayModel.count) return var row = displayModel.get(index) @@ -406,6 +434,7 @@ Item { boundsBehavior: Flickable.StopAtBounds delegate: Rectangle { + id: row required property int index required property string entryType required property string previewText @@ -455,14 +484,13 @@ Item { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onContainsMouseChanged: if (containsMouse) { - root.cursorActive = true - root.selectedIndex = index + onPositionChanged: function(mouse) { + root.selectFromPointer(row.index, row, mouse) } onClicked: { root.cursorActive = true - root.selectedIndex = index - root.activateIndex(index) + root.selectedIndex = row.index + root.activateIndex(row.index) } } } diff --git a/test/shell.d/clipboard-test.sh b/test/shell.d/clipboard-test.sh index 352599b6..3b6e2980 100644 --- a/test/shell.d/clipboard-test.sh +++ b/test/shell.d/clipboard-test.sh @@ -5,7 +5,9 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' +const fs = require('fs') const clipboard = requireFromRoot('shell/plugins/clipboard/ClipboardHistory.js') +const clipboardQml = fs.readFileSync(path.join(root, 'shell/plugins/clipboard/Clipboard.qml'), 'utf8') assertDeepEqual( clipboard.normalizeEntry('hello'), @@ -123,6 +125,27 @@ assertDeepEqual( assertDeepEqual(clipboard.displayRows(history, '', 0), [], 'clipboard display rows supports zero result limit') assertDeepEqual(clipboard.addEntry(history, 'next', 0), [], 'clipboard addEntry supports zero history limit') + +assert( + /function select\(delta\)[\s\S]*root\.disarmPointer\(\)[\s\S]*selectedIndex =/.test(clipboardQml), + 'clipboard keyboard navigation disarms pointer selection' +) +assert( + /function pointerMovedInCard\(item, mouse\)[\s\S]*item\.mapToItem\(card, mouse\.x, mouse\.y\)/.test(clipboardQml), + 'clipboard compares pointer movement in card coordinates' +) +assert( + /function selectFromPointer\(index, item, mouse\)[\s\S]*pointerMovedInCard\(item, mouse\)[\s\S]*root\.selectedIndex = index/.test(clipboardQml), + 'clipboard only selects from pointer after real movement' +) +assert( + /onPositionChanged: function\(mouse\) \{\s*root\.selectFromPointer\(row\.index, row, mouse\)\s*\}/.test(clipboardQml), + 'clipboard row hover routes through pointer movement gate' +) +assert( + !/onContainsMouseChanged:[\s\S]*root\.selectedIndex/.test(clipboardQml), + 'clipboard does not select rows from containsMouse' +) JS TMPDIR=$(mktemp -d)