From a0abb1e0d1589e6845a2283afdcc207aad2588a0 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 29 Jun 2026 11:40:31 -0500 Subject: [PATCH] Ignore stationary pointer hover in launcher --- shell/plugins/launcher/Launcher.qml | 39 ++++++++++++++++++++++------ test/shell.d/launcher-search-test.sh | 14 +++++++++- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/shell/plugins/launcher/Launcher.qml b/shell/plugins/launcher/Launcher.qml index d7221fe1..d4b09fc8 100644 --- a/shell/plugins/launcher/Launcher.qml +++ b/shell/plugins/launcher/Launcher.qml @@ -20,6 +20,8 @@ Item { property int selectedIndex: 0 property bool cursorActive: true property bool hoverArmed: false + property real lastPointerX: -1 + property real lastPointerY: -1 property var filteredEntries: [] property int launchSerial: 0 property int launchToplevelCount: 0 @@ -79,7 +81,7 @@ Item { root.filterText = payload.query || "" root.selectedIndex = 0 root.cursorActive = true - root.hoverArmed = false + root.disarmHover() root.rebuildDisplay() Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } @@ -130,6 +132,28 @@ Item { return LauncherSearch.entrySearchText(entry) } + function disarmHover() { + root.hoverArmed = false + 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.hoverArmed = true + root.cursorActive = true + root.selectedIndex = index + } + function isHiddenEntry(entry) { var id = String((entry && entry.id) || "") return root.configuredHiddenEntryIds[id] === true || root.desktopHiddenEntryIds[id] === true @@ -234,14 +258,14 @@ Item { root.filterText = nextFilter root.selectedIndex = 0 root.cursorActive = true - root.hoverArmed = false + root.disarmHover() root.rebuildDisplay() } function select(delta) { if (displayModel.count === 0) return root.cursorActive = true - root.hoverArmed = false + root.disarmHover() root.selectedIndex = (root.selectedIndex + delta + displayModel.count) % displayModel.count resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain) } @@ -272,6 +296,7 @@ Item { root.deleteConfirmOpen = false root.deleteEntry = null deleteConfirm.selectedIndex = 1 + root.disarmHover() Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } @@ -463,7 +488,7 @@ Item { } else if (event.key === Qt.Key_Home) { if (displayModel.count > 0) { root.cursorActive = true - root.hoverArmed = false + root.disarmHover() root.selectedIndex = 0 resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain) } @@ -471,7 +496,7 @@ Item { } else if (event.key === Qt.Key_End) { if (displayModel.count > 0) { root.cursorActive = true - root.hoverArmed = false + root.disarmHover() root.selectedIndex = displayModel.count - 1 resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain) } @@ -611,9 +636,7 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onPositionChanged: function(mouse) { - root.hoverArmed = true - root.cursorActive = true - root.selectedIndex = row.index + root.selectFromPointer(row.index, row, mouse) } onContainsMouseChanged: if (containsMouse && root.hoverArmed) { root.cursorActive = true diff --git a/test/shell.d/launcher-search-test.sh b/test/shell.d/launcher-search-test.sh index 74ddc652..efa51ffd 100644 --- a/test/shell.d/launcher-search-test.sh +++ b/test/shell.d/launcher-search-test.sh @@ -69,9 +69,21 @@ const directMatches = search.sortedEntries(entries, 'obs').map(row => search.ent assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works') assert( - /function select\(delta\)[\s\S]*root\.hoverArmed = false[\s\S]*root\.selectedIndex =/.test(launcherQml), + /function select\(delta\)[\s\S]*root\.disarmHover\(\)[\s\S]*root\.selectedIndex =/.test(launcherQml), 'launcher keyboard navigation disarms stale hover before moving selection' ) +assert( + /function pointerMovedInCard\(item, mouse\)[\s\S]*item\.mapToItem\(card, mouse\.x, mouse\.y\)/.test(launcherQml), + 'launcher compares pointer movement in card coordinates' +) +assert( + /function selectFromPointer\(index, item, mouse\)[\s\S]*pointerMovedInCard\(item, mouse\)[\s\S]*root\.selectedIndex = index/.test(launcherQml), + 'launcher only selects from pointer after real movement' +) +assert( + /onPositionChanged: function\(mouse\) \{\s*root\.selectFromPointer\(row\.index, row, mouse\)\s*\}/.test(launcherQml), + 'launcher row hover routes through pointer movement gate' +) const confirmDeleteMatch = launcherQml.match(/function confirmDelete\(\) \{([\s\S]*?)\n \}/) assert(confirmDeleteMatch, 'launcher confirmDelete function exists')