Dim installed software in the Install menus instead of hiding it (#6955)

Install rows hid themselves with `when:"! <present>"`, so software you
already had vanished from the very list it was installed from. Add a
`disabled:` guard that keeps a row listed but dim, ✓-marked, unselectable
and out of search, and move every Install row onto it.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-15 15:31:07 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent b724f76156
commit 4b93f8d84d
5 changed files with 255 additions and 91 deletions
+69 -16
View File
@@ -317,6 +317,7 @@ Item {
aliases: aliases,
when: "",
checked: "",
disabled: "",
order: 0
})
}
@@ -382,6 +383,7 @@ Item {
aliases: [],
when: "",
checked: "",
disabled: "",
order: 0
})
}
@@ -470,9 +472,10 @@ Item {
return MenuModel.isVisible(root.items, root.itemOrder, root.whenResults, entry)
}
// Label with the ✓ marker baked in when `checked:` evaluated truthy.
// Label with the ✓ marker baked in when `checked:` or `disabled:` evaluated
// truthy.
function labelFor(entry) {
return MenuModel.labelFor(entry, root.checkedResults)
return MenuModel.labelFor(entry, root.checkedResults, root.disabledResults)
}
function searchableToken(value) {
@@ -495,8 +498,17 @@ Item {
return MenuModel.descriptionTextMatches(query, text)
}
// Rows whose `disabled:` evaluated truthy stay listed but dimmed, and the
// cursor steps over them.
function isDisabled(entry) {
return MenuModel.isDisabled(root.disabledResults, entry)
}
// A disabled row earns its place in the submenu it belongs to, where the
// list around it is the point. Search is a list of what you can do, so it
// leaves them out.
function matchesQuery(entry, query) {
return MenuModel.matchesQuery(entry, query, root.isVisible(entry))
return MenuModel.matchesQuery(entry, query, root.isVisible(entry) && !root.isDisabled(entry))
}
function searchScore(entry, query) {
@@ -504,7 +516,38 @@ Item {
}
function displayRow(entry, detail, score, section) {
return MenuModel.displayRow(root.items, root.itemOrder, root.checkedResults, entry, detail, score, section)
return MenuModel.displayRow(root.items, root.itemOrder, root.checkedResults, root.disabledResults, entry, detail, score, section)
}
function rowSelectable(index) {
if (index < 0 || index >= displayModel.count) return false
return !displayModel.get(index).disabled
}
// First selectable row at or past `from`, continuing in the direction of
// travel and wrapping. -1 when every row is disabled, which leaves the menu
// with no cursor at all rather than one parked on a row Enter won't run.
function nextSelectable(from, direction) {
var count = displayModel.count
if (count === 0) return -1
var step = direction < 0 ? -1 : 1
var index = ((from % count) + count) % count
for (var i = 0; i < count; i++) {
if (root.rowSelectable(index)) return index
index = (index + step + count) % count
}
return -1
}
// Park the cursor on a selectable row after the rows underneath it changed.
// A menu with nothing selectable in it -- every app in it already installed
// -- shows no cursor at all, and grows one the moment a row can take it.
function settleCursor() {
var target = root.nextSelectable(root.selectedIndex, 1)
root.selectedIndex = target >= 0 ? target : 0
root.cursorActive = target >= 0
}
function rebuildDmenuDisplay() {
@@ -530,6 +573,7 @@ Item {
&& detail.toLowerCase().indexOf(query) < 0) continue
displayModel.append({
itemId: "dmenu." + i,
disabled: false,
kind: "dmenu",
icon: icon,
iconFont: "",
@@ -630,9 +674,7 @@ Item {
for (var k = 0; k < rows.length; k++) displayModel.append(rows[k])
layoutSerial += 1
if (displayModel.count === 0) selectedIndex = 0
else if (selectedIndex >= displayModel.count) selectedIndex = displayModel.count - 1
else if (selectedIndex < 0) selectedIndex = 0
root.settleCursor()
Qt.callLater(function() {
if (displayModel.count > 0) root.revealCursor()
@@ -665,12 +707,12 @@ Item {
if (displayModel.count === 0) return
root.disarmPointer()
if (!cursorActive) {
cursorActive = true
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
} else {
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count
}
var from = cursorActive ? selectedIndex + delta : (delta < 0 ? displayModel.count - 1 : 0)
var target = root.nextSelectable(from, delta)
if (target < 0) return
cursorActive = true
selectedIndex = target
revealCursor()
}
@@ -727,7 +769,7 @@ Item {
return
}
if (index < 0 || index >= displayModel.count) return
if (!root.rowSelectable(index)) return
var row = displayModel.get(index)
if (row.kind === "menu" || row.kind === "link") {
@@ -874,6 +916,7 @@ Item {
function selectFromPointer(index, item, mouse) {
if (!pointerGate.moved(item, mouse)) return
if (!root.rowSelectable(index)) return
root.cursorActive = true
root.selectedIndex = index
}
@@ -947,6 +990,7 @@ Item {
property var whenResults: ({}) // id → true|false (allow visibility)
property var checkedResults: ({}) // id → true|false (show ✓)
property var disabledResults: ({}) // id → true|false (dim, skip cursor)
property bool guardsPending: false
function evaluateGuards() {
@@ -966,6 +1010,7 @@ Item {
if (!script) {
root.whenResults = ({})
root.checkedResults = ({})
root.disabledResults = ({})
return
}
guardProc.collected = ""
@@ -991,6 +1036,7 @@ Item {
var nextWhen = ({})
var nextChecked = ({})
var nextDisabled = ({})
var lines = guardProc.collected.split("\n")
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
@@ -1005,9 +1051,11 @@ Item {
var tag = rest.substring(tagAt + 1)
if (tag === "w") nextWhen[id] = value
else if (tag === "c") nextChecked[id] = value
else if (tag === "d") nextDisabled[id] = value
}
root.whenResults = nextWhen
root.checkedResults = nextChecked
root.disabledResults = nextDisabled
if (root.opened) root.rebuildDisplay()
// Run the evaluation that had to stand aside. Deferred by a turn so the
// process is settled before its command is set again.
@@ -1108,7 +1156,7 @@ Item {
if (root.mode === "input") root.applyDmenuSelection(root.filterText)
else if (displayModel.count > 0) root.activateIndex(root.cursorActive ? root.selectedIndex : 0)
} else if (root.cursorActive) root.activateIndex(root.selectedIndex)
else if (displayModel.count > 0) root.cursorActive = true
else root.settleCursor()
event.accepted = true
} else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) {
root.setFilter(root.filterText + event.text)
@@ -1211,6 +1259,7 @@ Item {
required property string path
required property string action
required property int childCount
required property bool disabled
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
readonly property bool isApp: row.kind === "app"
@@ -1218,6 +1267,9 @@ Item {
width: ListView.view.width
height: root.rowHeightForDetail(row.detail)
// Faded: the row is here to say the software is already
// installed, not to be picked.
opacity: row.disabled ? 0.4 : 1
radius: root.cornerRadius
color: row.hasCursor ? root.selectedBackground : "transparent"
borderSpec: row.hasCursor ? root.selectedBorderSpec : Border.none()
@@ -1330,7 +1382,7 @@ Item {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
cursorShape: row.disabled ? Qt.ArrowCursor : Qt.PointingHandCursor
onEntered: root.selectFromPointer(row.index, row, {
x: mouseArea.mouseX,
y: mouseArea.mouseY
@@ -1339,6 +1391,7 @@ Item {
root.selectFromPointer(row.index, row, mouse)
}
onClicked: {
if (row.disabled) return
root.cursorActive = true
root.selectedIndex = row.index
root.activateIndex(row.index, true)