Combine the Omarchy menu and the launcher
Now that we can deep search, they don't need to be different
This commit is contained in:
@@ -14,7 +14,6 @@ User-installed plugins live alongside these conceptually but on disk under
|
||||
| Plugin | id | kinds | entry point |
|
||||
|---------------|---------------------------|-------------------------|---------------------------------------|
|
||||
| Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` |
|
||||
| Launcher | `omarchy.launcher` | `overlay` | `launcher/Launcher.qml` |
|
||||
| Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` |
|
||||
| Emojis | `omarchy.emojis` | `overlay` | `emojis/Emojis.qml` |
|
||||
| Clipboard mgr | `omarchy.clipboard` | `overlay` | `clipboard/Clipboard.qml` |
|
||||
@@ -49,14 +48,6 @@ providing [`config/omarchy/shell.json`](../../config/omarchy/shell.json) when
|
||||
the user has no file). See [`bar/README.md`](bar/README.md) for the widget catalogue
|
||||
and customization schema.
|
||||
|
||||
## Launcher
|
||||
|
||||
Quickshell-powered launcher. It uses Quickshell's native
|
||||
`DesktopEntries` model for discovery/activation and renders inside the
|
||||
long-running shell with the legacy launcher card dimensions, colors, row
|
||||
spacing, icon sizing, and keyboard behavior. Summoned directly over shell IPC
|
||||
by the `SUPER + SPACE` binding and the Omarchy menu Apps row.
|
||||
|
||||
## Image picker
|
||||
|
||||
Fullscreen image-grid selector overlay. Used by `omarchy-menu-images`
|
||||
|
||||
@@ -1,665 +0,0 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
import "LauncherSearch.js" as LauncherSearch
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||
property var shell: null
|
||||
property var manifest: null
|
||||
|
||||
property bool opened: false
|
||||
property string placeholder: "\uf002 Search..."
|
||||
property string filterText: ""
|
||||
property int selectedIndex: 0
|
||||
property bool cursorActive: true
|
||||
property var filteredEntries: []
|
||||
property int launchSerial: 0
|
||||
property int launchToplevelCount: 0
|
||||
property var launchActiveToplevel: null
|
||||
property bool launchOsdOpen: false
|
||||
property string launchOsdMessage: ""
|
||||
property var configuredHiddenEntryIds: ({})
|
||||
property var desktopHiddenEntryIds: ({})
|
||||
property bool deleteConfirmOpen: false
|
||||
property var deleteEntry: null
|
||||
|
||||
// Maps an icon name to a file on disk (e.g. "omacut" -> ".../apps/omacut.svg").
|
||||
// Used as a fallback for icons that Qt's themed lookup misses because they were
|
||||
// installed after this process started (its icon cache never re-scans). Refreshed
|
||||
// whenever the app list changes, so newly installed apps get their icon live.
|
||||
property var iconIndex: ({})
|
||||
property var pendingIconIndex: ({})
|
||||
|
||||
// Bound to the central [launcher] section in shell.toml via Color.qml.
|
||||
// Each color already includes its alpha companion (composed in the
|
||||
// singleton), so consumers can drop them straight into a Rectangle.
|
||||
property color background: Color.launcher.background
|
||||
property color foreground: Color.launcher.text
|
||||
property color border: Color.launcher.border
|
||||
property var borderSpec: Border.surfaceSpec("launcher", "border", border, 2)
|
||||
property color scrim: Color.launcher.scrim
|
||||
property color selectedBackground: Color.launcher.selectedBackground
|
||||
property color selectedText: Color.launcher.selectedText
|
||||
property color selectedBorder: Color.launcher.selectedBorder
|
||||
property var selectedBorderSpec: Border.surfaceSpec("launcher", "selected-border", selectedBorder, 0)
|
||||
readonly property real rowReservedBorderLeft: Border.left(selectedBorderSpec)
|
||||
readonly property real rowReservedBorderRight: Border.right(selectedBorderSpec)
|
||||
property string fontFamily: Style.font.menuFamily
|
||||
|
||||
property int cardWidth: 644
|
||||
property int cardHeight: 400
|
||||
property int contentMargin: 20
|
||||
property int contentSpacing: 10
|
||||
property int searchHeight: 44
|
||||
property int rowHeight: 50
|
||||
property int iconSlotWidth: 44
|
||||
property int iconSize: 24
|
||||
readonly property int listHeight: cardHeight - contentMargin * 2 - searchHeight - contentSpacing
|
||||
|
||||
function open(payloadJson) {
|
||||
var payload = ({})
|
||||
try { payload = JSON.parse(payloadJson || "{}") } catch (e) { payload = ({}) }
|
||||
|
||||
root.placeholder = payload.placeholder || "\uf002 Search..."
|
||||
root.cardWidth = Math.max(300, Number(payload.width || 644))
|
||||
var requestedListHeight = Number(payload.listHeight || payload.maxHeight || 0)
|
||||
root.cardHeight = requestedListHeight > 0
|
||||
? root.contentMargin * 2 + root.searchHeight + root.contentSpacing + requestedListHeight
|
||||
: 400
|
||||
|
||||
root.filterText = payload.query || ""
|
||||
root.selectedIndex = 0
|
||||
root.cursorActive = true
|
||||
root.disarmHover()
|
||||
root.opened = true
|
||||
root.rebuildDisplay()
|
||||
// The shell may start before first-install packages have finished placing
|
||||
// their icons. Refresh here even when the desktop entry list did not change.
|
||||
if (!iconIndexScan.running) iconIndexScan.running = true
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
|
||||
function close() {
|
||||
root.opened = false
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
root.deleteConfirmOpen = false
|
||||
root.deleteEntry = null
|
||||
root.opened = false
|
||||
if (root.shell && typeof root.shell.hide === "function")
|
||||
root.shell.hide((root.manifest && root.manifest.id) || "omarchy.launcher")
|
||||
}
|
||||
|
||||
function iconSource(icon) {
|
||||
var value = String(icon || "")
|
||||
if (value.length === 0) return Quickshell.iconPath("application-x-executable", true)
|
||||
if (value.indexOf("file://") === 0 || value.indexOf("image://") === 0) return value
|
||||
if (value.charAt(0) === "/") return Util.fileUrl(value)
|
||||
// Prefer the context-limited app/device index. An unconstrained themed
|
||||
// lookup can resolve an app name such as "zoom" to an action icon instead.
|
||||
var found = root.iconIndex[value]
|
||||
if (found) return Util.fileUrl(found)
|
||||
var themed = Quickshell.iconPath(value, true)
|
||||
if (themed.length > 0) return themed
|
||||
return Quickshell.iconPath("application-x-executable", true)
|
||||
}
|
||||
|
||||
function entryName(entry) {
|
||||
return LauncherSearch.entryName(entry)
|
||||
}
|
||||
|
||||
function entrySubtext(entry) {
|
||||
return LauncherSearch.entrySubtext(entry)
|
||||
}
|
||||
|
||||
function entrySortKey(entry) {
|
||||
return LauncherSearch.entrySortKey(entry)
|
||||
}
|
||||
|
||||
function toplevelCount() {
|
||||
try { return ToplevelManager.toplevels.values.length } catch (e) { return 0 }
|
||||
}
|
||||
|
||||
function entrySearchText(entry) {
|
||||
return LauncherSearch.entrySearchText(entry)
|
||||
}
|
||||
|
||||
function disarmHover() {
|
||||
pointerGate.reset()
|
||||
}
|
||||
|
||||
function selectFromPointer(index, item, mouse) {
|
||||
if (!pointerGate.moved(item, mouse)) return
|
||||
root.cursorActive = true
|
||||
root.selectedIndex = index
|
||||
}
|
||||
|
||||
function isHiddenEntry(entry) {
|
||||
var id = String((entry && entry.id) || "")
|
||||
return root.configuredHiddenEntryIds[id] === true || root.desktopHiddenEntryIds[id] === true
|
||||
}
|
||||
|
||||
function normalizeDesktopId(id) {
|
||||
var value = String(id || "").trim()
|
||||
if (value.slice(-8) === ".desktop") value = value.slice(0, -8)
|
||||
return value
|
||||
}
|
||||
|
||||
function loadConfiguredHides(rawText) {
|
||||
var next = ({})
|
||||
var lines = String(rawText || "").split(/\n/)
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var id = root.normalizeDesktopId(lines[i])
|
||||
if (id.length > 0) next[id] = true
|
||||
}
|
||||
root.configuredHiddenEntryIds = next
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
|
||||
function loadDesktopHiddenEntries(rawText) {
|
||||
var next = ({})
|
||||
var lines = String(rawText || "").split(/\n/)
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var id = root.normalizeDesktopId(lines[i])
|
||||
if (id.length > 0) next[id] = true
|
||||
}
|
||||
root.desktopHiddenEntryIds = next
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
|
||||
function iconIndexScanCommand() {
|
||||
// List app/device icons across the XDG icon dirs and /usr/share/pixmaps as
|
||||
// "<path>" lines. Some desktop entries, such as Print Settings, use device
|
||||
// icons like "printer" instead of app icons. SVGs are emitted before PNGs
|
||||
// so the parser, which keeps the first hit per name, prefers scalable icons.
|
||||
return [
|
||||
'dirs="$HOME/.icons $HOME/.local/share/icons";',
|
||||
'IFS=":"; for d in ${XDG_DATA_DIRS:-/usr/local/share:/usr/share}; do dirs="$dirs $d/icons"; done; unset IFS;',
|
||||
'for ext in svg png; do',
|
||||
' for base in $dirs; do',
|
||||
' [[ -d $base ]] && find "$base" \\( -path "*/apps/*" -o -path "*/devices/*" \\) -name "*.$ext" 2>/dev/null;',
|
||||
' done;',
|
||||
' find /usr/share/pixmaps -maxdepth 1 -name "*.$ext" 2>/dev/null;',
|
||||
'done'
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
function indexIconLine(path) {
|
||||
var value = String(path || "").trim()
|
||||
if (value.length === 0) return
|
||||
var slash = value.lastIndexOf("/")
|
||||
var file = slash >= 0 ? value.slice(slash + 1) : value
|
||||
var dot = file.lastIndexOf(".")
|
||||
var name = dot > 0 ? file.slice(0, dot) : file
|
||||
if (name.length > 0 && root.pendingIconIndex[name] === undefined)
|
||||
root.pendingIconIndex[name] = value
|
||||
}
|
||||
|
||||
function hiddenEntryScanCommand() {
|
||||
var desktop = [Quickshell.env("XDG_CURRENT_DESKTOP"), Quickshell.env("XDG_SESSION_DESKTOP"), Quickshell.env("DESKTOP_SESSION")].filter(function(v) { return String(v || "").length > 0 }).join(":")
|
||||
var script = root.omarchyPath + "/shell/plugins/launcher/hidden-entries.sh"
|
||||
return Util.shellQuote(script) + " " + Util.shellQuote(desktop)
|
||||
}
|
||||
|
||||
function fuzzyScore(entry, query) {
|
||||
return LauncherSearch.fuzzyScore(entry, query)
|
||||
}
|
||||
|
||||
function sortedEntries(query) {
|
||||
var values = DesktopEntries.applications.values || []
|
||||
return LauncherSearch.sortedEntries(values, query, function(entry) { return root.isHiddenEntry(entry) })
|
||||
}
|
||||
|
||||
function rebuildDisplay() {
|
||||
displayModel.clear()
|
||||
var rows = root.sortedEntries(root.filterText)
|
||||
var entries = []
|
||||
var count = Math.min(rows.length, 256)
|
||||
for (var i = 0; i < count; i++) {
|
||||
var entry = rows[i].entry
|
||||
entries.push(entry)
|
||||
displayModel.append({
|
||||
name: root.entryName(entry),
|
||||
subtext: root.entrySubtext(entry),
|
||||
icon: String(entry.icon || "")
|
||||
})
|
||||
}
|
||||
root.filteredEntries = entries
|
||||
|
||||
if (displayModel.count === 0) root.selectedIndex = 0
|
||||
else if (root.selectedIndex >= displayModel.count) root.selectedIndex = displayModel.count - 1
|
||||
else if (root.selectedIndex < 0) root.selectedIndex = 0
|
||||
|
||||
Qt.callLater(function() {
|
||||
if (displayModel.count > 0) resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
})
|
||||
}
|
||||
|
||||
function setFilter(nextFilter) {
|
||||
root.filterText = nextFilter
|
||||
root.selectedIndex = 0
|
||||
root.cursorActive = true
|
||||
root.disarmHover()
|
||||
root.rebuildDisplay()
|
||||
}
|
||||
|
||||
function select(delta) {
|
||||
if (displayModel.count === 0) return
|
||||
root.cursorActive = true
|
||||
root.disarmHover()
|
||||
root.selectedIndex = (root.selectedIndex + delta + displayModel.count) % displayModel.count
|
||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
function activateIndex(index) {
|
||||
if (root.deleteConfirmOpen) return
|
||||
if (index < 0 || index >= root.filteredEntries.length) return
|
||||
var entry = root.filteredEntries[index]
|
||||
if (!entry) return
|
||||
var desktopId = String(entry.id || "")
|
||||
if (!desktopId) return
|
||||
|
||||
root.beginLaunchFeedback(entry)
|
||||
root.dismiss()
|
||||
Util.execDetached("gtk-launch " + Util.shellQuote(desktopId))
|
||||
}
|
||||
|
||||
function requestDeleteIndex(index) {
|
||||
if (index < 0 || index >= root.filteredEntries.length) return
|
||||
var entry = root.filteredEntries[index]
|
||||
if (!entry) return
|
||||
root.deleteEntry = entry
|
||||
deleteConfirm.selectedIndex = 1
|
||||
root.deleteConfirmOpen = true
|
||||
}
|
||||
|
||||
function cancelDelete() {
|
||||
root.deleteConfirmOpen = false
|
||||
root.deleteEntry = null
|
||||
deleteConfirm.selectedIndex = 1
|
||||
root.disarmHover()
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
var entry = root.deleteEntry
|
||||
if (!entry) return
|
||||
|
||||
var desktopId = String(entry.id || "")
|
||||
var name = root.entryName(entry)
|
||||
var command = Util.shellQuote(root.omarchyPath + "/bin/omarchy-remove-launcher-entry") + " " + Util.shellQuote(desktopId) + " " + Util.shellQuote(name)
|
||||
root.dismiss()
|
||||
Util.execDetached(command)
|
||||
}
|
||||
|
||||
function beginLaunchFeedback(entry) {
|
||||
root.launchSerial++
|
||||
root.launchToplevelCount = root.toplevelCount()
|
||||
root.launchActiveToplevel = ToplevelManager.activeToplevel
|
||||
root.launchOsdOpen = false
|
||||
root.launchOsdMessage = "Launching " + root.entryName(entry) + "…"
|
||||
launchDelay.restart()
|
||||
launchTimeout.restart()
|
||||
}
|
||||
|
||||
function closeLaunchFeedback(serial) {
|
||||
if (serial !== root.launchSerial) return
|
||||
launchDelay.stop()
|
||||
launchTimeout.stop()
|
||||
if (root.launchOsdOpen) {
|
||||
Quickshell.execDetached(["omarchy-shell", "osd", "close"])
|
||||
root.launchOsdOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
function maybeFinishLaunchFeedback() {
|
||||
if (!launchDelay.running && !launchTimeout.running && !root.launchOsdOpen) return
|
||||
if (root.toplevelCount() <= root.launchToplevelCount && ToplevelManager.activeToplevel === root.launchActiveToplevel) return
|
||||
root.closeLaunchFeedback(root.launchSerial)
|
||||
}
|
||||
|
||||
ListModel { id: displayModel }
|
||||
|
||||
Process {
|
||||
id: hiddenEntryScan
|
||||
command: ["bash", "-lc", root.hiddenEntryScanCommand()]
|
||||
stdout: SplitParser { onRead: function(line) { hiddenEntryOutput.text += line + "\n" } }
|
||||
onStarted: hiddenEntryOutput.text = ""
|
||||
onExited: root.loadDesktopHiddenEntries(hiddenEntryOutput.text)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: iconIndexScan
|
||||
command: ["bash", "-lc", root.iconIndexScanCommand()]
|
||||
stdout: SplitParser { onRead: function(line) { root.indexIconLine(line) } }
|
||||
onStarted: root.pendingIconIndex = ({})
|
||||
// Swapping the property re-evaluates every iconSource() binding, so
|
||||
// newly found icons appear without rebuilding the list.
|
||||
onExited: root.iconIndex = root.pendingIconIndex
|
||||
}
|
||||
|
||||
// Coalesces bursts of app-list changes (a package install touches many
|
||||
// entries) into a single rescan.
|
||||
Timer {
|
||||
id: iconIndexDebounce
|
||||
interval: 750
|
||||
onTriggered: if (!iconIndexScan.running) iconIndexScan.running = true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: hiddenEntryOutput
|
||||
property string text: ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: launcherHidesFile
|
||||
path: root.omarchyPath + "/default/omarchy/launcher.hides"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onLoaded: root.loadConfiguredHides(text())
|
||||
onFileChanged: root.loadConfiguredHides(text())
|
||||
onLoadFailed: root.loadConfiguredHides("")
|
||||
}
|
||||
|
||||
PointerMoveGate {
|
||||
id: pointerGate
|
||||
referenceItem: card
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ToplevelManager.toplevels
|
||||
function onValuesChanged() { root.maybeFinishLaunchFeedback() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ToplevelManager
|
||||
function onActiveToplevelChanged() { root.maybeFinishLaunchFeedback() }
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: launchDelay
|
||||
interval: 2000
|
||||
onTriggered: {
|
||||
if (root.toplevelCount() > root.launchToplevelCount || ToplevelManager.activeToplevel !== root.launchActiveToplevel) return
|
||||
root.launchOsdOpen = true
|
||||
Quickshell.execDetached(["omarchy-shell", "osd", "show", JSON.stringify({ icon: "", message: root.launchOsdMessage, duration: 0 })])
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: launchTimeout
|
||||
interval: 15000
|
||||
onTriggered: root.closeLaunchFeedback(root.launchSerial)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries.applications
|
||||
function onValuesChanged() {
|
||||
hiddenEntryScan.running = true
|
||||
iconIndexDebounce.restart()
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
hiddenEntryScan.running = true
|
||||
iconIndexScan.running = true
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: panel
|
||||
visible: root.opened
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "omarchy-launcher"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.scrim
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.dismiss()
|
||||
}
|
||||
|
||||
BorderSurface {
|
||||
id: card
|
||||
width: Math.min(root.cardWidth, panel.width - Style.gapsOut * 2)
|
||||
height: Math.min(root.cardHeight, panel.height - Style.gapsOut * 2)
|
||||
radius: Style.cornerRadius
|
||||
anchors.centerIn: parent
|
||||
color: root.background
|
||||
borderSpec: root.borderSpec
|
||||
padding: root.contentMargin
|
||||
clip: true
|
||||
|
||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
z: root.deleteConfirmOpen ? 20 : 0
|
||||
focus: true
|
||||
|
||||
Keys.priority: Keys.BeforeItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (root.deleteConfirmOpen) {
|
||||
if (deleteConfirm.handleKey(event)) event.accepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (root.filterText.length > 0) root.setFilter("")
|
||||
else root.dismiss()
|
||||
event.accepted = true
|
||||
} else if (Util.editsFilter(event, root.filterText)) {
|
||||
root.setFilter(Util.editedFilter(event, root.filterText))
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
root.select(-1)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
root.select(1)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
root.select(-6)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_PageDown) {
|
||||
root.select(6)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Home) {
|
||||
if (displayModel.count > 0) {
|
||||
root.cursorActive = true
|
||||
root.disarmHover()
|
||||
root.selectedIndex = 0
|
||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_End) {
|
||||
if (displayModel.count > 0) {
|
||||
root.cursorActive = true
|
||||
root.disarmHover()
|
||||
root.selectedIndex = displayModel.count - 1
|
||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.activateIndex(root.selectedIndex)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Delete) {
|
||||
root.requestDeleteIndex(root.selectedIndex)
|
||||
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)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDialog {
|
||||
id: deleteConfirm
|
||||
|
||||
anchors.fill: parent
|
||||
opened: root.deleteConfirmOpen
|
||||
z: 10
|
||||
message: "Do you want to uninstall " + root.entryName(root.deleteEntry) + "?"
|
||||
confirmText: "Uninstall"
|
||||
background: root.background
|
||||
foreground: root.foreground
|
||||
scrim: root.scrim
|
||||
selectedBackground: root.selectedBackground
|
||||
selectedText: root.selectedText
|
||||
fontFamily: root.fontFamily
|
||||
cornerRadius: Style.cornerRadius
|
||||
onCanceled: root.cancelDelete()
|
||||
onConfirmed: root.confirmDelete()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: card.contentTopInset
|
||||
anchors.rightMargin: card.contentRightInset
|
||||
anchors.bottomMargin: card.contentBottomInset
|
||||
anchors.leftMargin: card.contentLeftInset
|
||||
spacing: root.contentSpacing
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: root.searchHeight
|
||||
radius: 0
|
||||
color: root.background
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.filterText || root.placeholder
|
||||
color: root.foreground
|
||||
opacity: root.filterText ? 1 : 0.5
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - root.searchHeight - root.contentSpacing
|
||||
|
||||
ListView {
|
||||
id: resultList
|
||||
anchors.fill: parent
|
||||
model: displayModel
|
||||
clip: true
|
||||
spacing: 0
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
delegate: BorderSurface {
|
||||
id: row
|
||||
required property int index
|
||||
required property string name
|
||||
required property string subtext
|
||||
required property string icon
|
||||
|
||||
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
|
||||
|
||||
width: ListView.view.width
|
||||
height: root.rowHeight
|
||||
radius: 0
|
||||
color: row.hasCursor ? root.selectedBackground : "transparent"
|
||||
borderSpec: row.hasCursor ? root.selectedBorderSpec : Border.none()
|
||||
|
||||
Item {
|
||||
id: iconSlot
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.rowReservedBorderLeft + 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.iconSlotWidth
|
||||
height: parent.height
|
||||
|
||||
Image {
|
||||
id: appIcon
|
||||
anchors.centerIn: parent
|
||||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
fillMode: Image.PreserveAspectFit
|
||||
// Decode at physical pixels: IconImage uses the logical size,
|
||||
// which leaves PNG icons upscaled and blurry on HiDPI displays.
|
||||
sourceSize.width: root.iconSize * Screen.devicePixelRatio
|
||||
sourceSize.height: root.iconSize * Screen.devicePixelRatio
|
||||
source: root.iconSource(row.icon)
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: appIcon.status === Image.Error
|
||||
text: "?"
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: iconSlot.right
|
||||
anchors.leftMargin: 14
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.rowReservedBorderRight + 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: row.name
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: root.selectFromPointer(row.index, row, {
|
||||
x: mouseArea.mouseX,
|
||||
y: mouseArea.mouseY
|
||||
})
|
||||
onPositionChanged: function(mouse) {
|
||||
root.selectFromPointer(row.index, row, mouse)
|
||||
}
|
||||
onClicked: {
|
||||
root.cursorActive = true
|
||||
root.selectedIndex = row.index
|
||||
root.activateIndex(row.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 14
|
||||
visible: displayModel.count === 0
|
||||
text: "No Results"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
function entryName(entry) {
|
||||
return String((entry && entry.name) || (entry && entry.id) || "")
|
||||
}
|
||||
|
||||
function entrySubtext(entry) {
|
||||
return String((entry && entry.genericName) || "")
|
||||
}
|
||||
|
||||
function entrySortKey(entry) {
|
||||
return entryName(entry).toLowerCase()
|
||||
}
|
||||
|
||||
function keywordText(entry) {
|
||||
try {
|
||||
if (entry && entry.keywords && typeof entry.keywords.join === "function") return entry.keywords.join(" ")
|
||||
} catch (e) {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function entrySearchText(entry) {
|
||||
if (!entry) return ""
|
||||
return [entry.name, entry.genericName, entry.comment, keywordText(entry), entry.id].join(" ").toLowerCase()
|
||||
}
|
||||
|
||||
function wordText(value) {
|
||||
return String(value || "")
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
||||
.replace(/[._:/\\-]+/g, " ")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function words(value) {
|
||||
var values = wordText(value).split(/[^a-z0-9]+/)
|
||||
var result = []
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
if (values[i]) result.push(values[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function entryAcronym(entry) {
|
||||
var values = words([entry && entry.name, entry && entry.genericName, keywordText(entry), entry && entry.id].join(" "))
|
||||
var result = ""
|
||||
for (var i = 0; i < values.length; i++) result += values[i].charAt(0)
|
||||
return result
|
||||
}
|
||||
|
||||
function termMatches(entry, term) {
|
||||
if (!term) return true
|
||||
|
||||
var name = entryName(entry).toLowerCase()
|
||||
var id = String((entry && entry.id) || "").toLowerCase()
|
||||
var haystack = entrySearchText(entry)
|
||||
|
||||
if (name.indexOf(term) >= 0) return true
|
||||
if (id.indexOf(term) >= 0) return true
|
||||
if (haystack.indexOf(term) >= 0) return true
|
||||
|
||||
return term.length <= 5 && entryAcronym(entry).indexOf(term) >= 0
|
||||
}
|
||||
|
||||
function allTermsMatch(entry, query) {
|
||||
var terms = String(query || "").toLowerCase().trim().split(/\s+/)
|
||||
for (var i = 0; i < terms.length; i++) {
|
||||
if (terms[i] && !termMatches(entry, terms[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function fuzzyScore(entry, query) {
|
||||
var q = String(query || "").trim().toLowerCase()
|
||||
if (!q) return 0
|
||||
if (!allTermsMatch(entry, q)) return -1
|
||||
|
||||
var name = entryName(entry).toLowerCase()
|
||||
var id = String((entry && entry.id) || "").toLowerCase()
|
||||
var haystack = entrySearchText(entry)
|
||||
var directName = name.indexOf(q)
|
||||
var directId = id.indexOf(q)
|
||||
if (directName === 0) return 10000 - name.length
|
||||
if (directId === 0) return 9500 - id.length
|
||||
if (directName > 0) return 8000 - directName * 10 - name.length
|
||||
if (directId > 0) return 7600 - directId * 10 - id.length
|
||||
|
||||
var hayIndex = haystack.indexOf(q)
|
||||
if (hayIndex >= 0) return 6000 - hayIndex
|
||||
|
||||
var acronym = entryAcronym(entry)
|
||||
var acronymIndex = acronym.indexOf(q)
|
||||
if (acronymIndex === 0) return 5000 - acronym.length
|
||||
if (acronymIndex > 0) return 4600 - acronymIndex * 10 - acronym.length
|
||||
|
||||
return 4000 - name.length
|
||||
}
|
||||
|
||||
function sortedEntries(values, query, hiddenCallback) {
|
||||
var q = String(query || "").trim()
|
||||
var rows = []
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var entry = values[i]
|
||||
if (!entry || entry.noDisplay) continue
|
||||
if (hiddenCallback && hiddenCallback(entry)) continue
|
||||
var name = entryName(entry)
|
||||
if (!name) continue
|
||||
var score = fuzzyScore(entry, q)
|
||||
if (score < 0) continue
|
||||
rows.push({ entry: entry, score: score, key: entrySortKey(entry), name: name.toLowerCase() })
|
||||
}
|
||||
|
||||
rows.sort(function(a, b) {
|
||||
if (q && a.score !== b.score) return b.score - a.score
|
||||
if (a.key < b.key) return -1
|
||||
if (a.key > b.key) return 1
|
||||
if (a.name < b.name) return -1
|
||||
if (a.name > b.name) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
entryName: entryName,
|
||||
entrySubtext: entrySubtext,
|
||||
entrySortKey: entrySortKey,
|
||||
entrySearchText: entrySearchText,
|
||||
entryAcronym: entryAcronym,
|
||||
fuzzyScore: fuzzyScore,
|
||||
sortedEntries: sortedEntries
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
desktop_names=${1:-}
|
||||
|
||||
declare -A seen_ids
|
||||
|
||||
desktop_matches() {
|
||||
local list=$1
|
||||
local desktop_name
|
||||
local entry
|
||||
|
||||
IFS=":" read -ra desktop_parts <<< "$desktop_names"
|
||||
IFS=";" read -ra entries <<< "$list"
|
||||
|
||||
for desktop_name in "${desktop_parts[@]}"; do
|
||||
[[ -z $desktop_name ]] && continue
|
||||
|
||||
for entry in "${entries[@]}"; do
|
||||
[[ -z $entry ]] && continue
|
||||
[[ $entry == "$desktop_name" ]] && return 0
|
||||
done
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
desktop_id_for_file() {
|
||||
local dir=$1
|
||||
local file=$2
|
||||
local rel=${file#"$dir"/}
|
||||
|
||||
rel=${rel%.desktop}
|
||||
printf '%s\n' "${rel//\//-}"
|
||||
}
|
||||
|
||||
is_hidden_desktop_file() {
|
||||
local file=$1
|
||||
local in_desktop_entry=0
|
||||
local hidden=false
|
||||
local only_show_in=
|
||||
local not_show_in=
|
||||
local line key value
|
||||
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
line=${line%$'\r'}
|
||||
|
||||
if [[ $line =~ ^\[(.*)\]$ ]]; then
|
||||
[[ ${BASH_REMATCH[1]} == "Desktop Entry" ]] && in_desktop_entry=1 || in_desktop_entry=0
|
||||
continue
|
||||
fi
|
||||
|
||||
(( in_desktop_entry )) || continue
|
||||
[[ $line == *=* ]] || continue
|
||||
|
||||
key=${line%%=*}
|
||||
value=${line#*=}
|
||||
|
||||
case $key in
|
||||
Hidden|NoDisplay)
|
||||
[[ $value == "true" ]] && hidden=true
|
||||
;;
|
||||
OnlyShowIn)
|
||||
only_show_in=$value
|
||||
;;
|
||||
NotShowIn)
|
||||
not_show_in=$value
|
||||
;;
|
||||
esac
|
||||
done < "$file"
|
||||
|
||||
[[ $hidden == "true" ]] && return 0
|
||||
[[ -n $only_show_in ]] && ! desktop_matches "$only_show_in" && return 0
|
||||
[[ -n $not_show_in ]] && desktop_matches "$not_show_in" && return 0
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
scan_dir() {
|
||||
local dir=$1
|
||||
local file id
|
||||
|
||||
[[ -d $dir ]] || return
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
id=$(desktop_id_for_file "$dir" "$file")
|
||||
[[ -n ${seen_ids[$id]+set} ]] && continue
|
||||
seen_ids[$id]=1
|
||||
|
||||
if is_hidden_desktop_file "$file"; then
|
||||
printf '%s\n' "$id"
|
||||
fi
|
||||
done < <(find "$dir" -type f -name '*.desktop' -print0 2>/dev/null | sort -z)
|
||||
}
|
||||
|
||||
scan_dir "$HOME/.local/share/applications"
|
||||
|
||||
IFS=":" read -ra data_dirs <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||
for data_dir in "${data_dirs[@]}"; do
|
||||
scan_dir "$data_dir/applications"
|
||||
done
|
||||
|
||||
scan_dir "$HOME/.nix-profile/share/applications"
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.launcher",
|
||||
"name": "Launcher",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "A Quickshell-powered launcher",
|
||||
"kinds": [
|
||||
"overlay"
|
||||
],
|
||||
"keepLoaded": true,
|
||||
"entryPoints": {
|
||||
"overlay": "Launcher.qml"
|
||||
}
|
||||
}
|
||||
+167
-3
@@ -11,6 +11,8 @@ Item {
|
||||
|
||||
// Injected by omarchy-shell when this plugin is summoned.
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||
property var shell: null
|
||||
property var manifest: null
|
||||
|
||||
// Plugin lifecycle hooks. The host calls open(payloadJson) after
|
||||
// `omarchy-shell shell summon omarchy.menu ...` and close() when hidden.
|
||||
@@ -72,6 +74,13 @@ Item {
|
||||
property var providersLoaded: ({})
|
||||
property var providerQueue: []
|
||||
property int providerRevision: 0
|
||||
|
||||
// Shared application engine (entries, hidden filters, icons, launch,
|
||||
// removal), owned by the shell and also used by the standalone launcher.
|
||||
readonly property var appLibrary: root.shell ? root.shell.appLibrary : null
|
||||
property bool deleteConfirmOpen: false
|
||||
property var deleteTarget: null
|
||||
onOpenedChanged: if (!opened) { deleteConfirmOpen = false; deleteTarget = null }
|
||||
// Bound to the central [menu] section in shell.toml via Color.qml.
|
||||
// Each color already includes its alpha companion (composed in the
|
||||
// singleton), so consumers can drop them straight into a Rectangle.
|
||||
@@ -231,9 +240,64 @@ Item {
|
||||
return MenuModel.slugify(value)
|
||||
}
|
||||
|
||||
// The apps provider is QML-native: rows come from the shared AppLibrary
|
||||
// (DesktopEntries) instead of a bash enumeration, so they carry image
|
||||
// icons, launch feedback, and uninstall support like the launcher.
|
||||
function mergeAppRows() {
|
||||
if (!root.appLibrary) return
|
||||
|
||||
var nextOrder = []
|
||||
for (var i = 0; i < root.itemOrder.length; i++) {
|
||||
var id = root.itemOrder[i]
|
||||
var existing = root.items[id]
|
||||
if (existing && existing.kind === "app") delete root.items[id]
|
||||
else nextOrder.push(id)
|
||||
}
|
||||
|
||||
var rows = root.appLibrary.sortedEntries("")
|
||||
for (var j = 0; j < rows.length; j++) {
|
||||
var entry = rows[j].entry
|
||||
var appId = String(entry.id || "")
|
||||
if (!appId) continue
|
||||
var itemId = "apps." + appId
|
||||
var subtext = root.appLibrary.entrySubtext(entry)
|
||||
var aliases = subtext ? [subtext] : []
|
||||
try {
|
||||
if (entry.keywords && typeof entry.keywords.join === "function") aliases = aliases.concat(entry.keywords)
|
||||
} catch (e) { }
|
||||
root.items[itemId] = {
|
||||
id: itemId,
|
||||
parent: "apps",
|
||||
kind: "app",
|
||||
icon: "",
|
||||
appIcon: String(entry.icon || ""),
|
||||
appId: appId,
|
||||
label: root.appLibrary.entryName(entry),
|
||||
title: "",
|
||||
target: "",
|
||||
description: subtext,
|
||||
action: "",
|
||||
provider: "",
|
||||
aliases: aliases,
|
||||
when: "",
|
||||
checked: "",
|
||||
order: nextOrder.length
|
||||
}
|
||||
nextOrder.push(itemId)
|
||||
}
|
||||
|
||||
root.itemOrder = nextOrder
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
|
||||
function startProviderForMenu(id) {
|
||||
var entry = root.item(id)
|
||||
if (!entry || !entry.provider || root.providersLoaded[id]) return
|
||||
if (entry.provider === "apps") {
|
||||
root.providersLoaded[id] = true
|
||||
root.mergeAppRows()
|
||||
return
|
||||
}
|
||||
var spec = root.providers[entry.provider]
|
||||
if (!spec) return
|
||||
|
||||
@@ -301,6 +365,12 @@ Item {
|
||||
var entry = root.item(id)
|
||||
if (!entry || !entry.provider || root.providersLoaded[id]) return
|
||||
|
||||
// Native providers don't touch providerProc, so they never need to queue.
|
||||
if (entry.provider === "apps") {
|
||||
root.startProviderForMenu(id)
|
||||
return
|
||||
}
|
||||
|
||||
if (providerProc.running) {
|
||||
if (root.providerQueue.indexOf(id) < 0) root.providerQueue = root.providerQueue.concat([id])
|
||||
return
|
||||
@@ -403,6 +473,8 @@ Item {
|
||||
kind: "dmenu",
|
||||
icon: "",
|
||||
iconFont: "",
|
||||
appIcon: "",
|
||||
appId: "",
|
||||
label: label,
|
||||
target: "",
|
||||
detail: "",
|
||||
@@ -544,6 +616,7 @@ Item {
|
||||
}
|
||||
|
||||
function activateIndex(index, fromPointer) {
|
||||
if (root.deleteConfirmOpen) return
|
||||
if (root.dmenuActive) {
|
||||
if (root.mode === "input") {
|
||||
root.applyDmenuSelection(root.filterText)
|
||||
@@ -559,11 +632,44 @@ Item {
|
||||
var row = displayModel.get(index)
|
||||
if (row.kind === "menu" || row.kind === "link") {
|
||||
root.setActiveMenu(row.target || row.itemId, true, fromPointer)
|
||||
} else if (row.kind === "app") {
|
||||
var appId = row.appId
|
||||
var label = row.label
|
||||
applySerial = requestSerial
|
||||
opened = false
|
||||
filterText = ""
|
||||
if (root.appLibrary) root.appLibrary.launch(appId, label)
|
||||
} else {
|
||||
root.applySelected(row.itemId, row.action)
|
||||
}
|
||||
}
|
||||
|
||||
function requestDeleteSelected() {
|
||||
if (!root.cursorActive || root.selectedIndex < 0 || root.selectedIndex >= displayModel.count) return
|
||||
var row = displayModel.get(root.selectedIndex)
|
||||
if (!row || row.kind !== "app") return
|
||||
root.deleteTarget = { appId: row.appId, label: row.label }
|
||||
deleteConfirm.selectedIndex = 1
|
||||
root.deleteConfirmOpen = true
|
||||
}
|
||||
|
||||
function cancelDelete() {
|
||||
root.deleteConfirmOpen = false
|
||||
root.deleteTarget = null
|
||||
deleteConfirm.selectedIndex = 1
|
||||
root.disarmPointer()
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
var target = root.deleteTarget
|
||||
root.deleteConfirmOpen = false
|
||||
root.deleteTarget = null
|
||||
if (!target) return
|
||||
root.cancel()
|
||||
if (root.appLibrary) root.appLibrary.remove(target.appId, target.label)
|
||||
}
|
||||
|
||||
function applyDmenuSelection(value) {
|
||||
applySerial = requestSerial
|
||||
opened = false
|
||||
@@ -602,6 +708,9 @@ Item {
|
||||
opened = true
|
||||
rebuildDisplay()
|
||||
loadProviderForMenu(activeMenu)
|
||||
// The shell may start before first-install packages have finished placing
|
||||
// their icons. Refresh here even when the desktop entry list did not change.
|
||||
if (root.appLibrary) root.appLibrary.refreshIcons()
|
||||
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
@@ -709,6 +818,13 @@ Item {
|
||||
referenceItem: card
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.appLibrary
|
||||
function onAppsChanged() {
|
||||
if (root.providersLoaded["apps"]) root.mergeAppRows()
|
||||
}
|
||||
}
|
||||
|
||||
// The JSONC sources are watched so live edits to the default file (or the
|
||||
// user extension at ~/.config/omarchy/extensions/omarchy-menu.jsonc) take
|
||||
// effect without restarting the shell.
|
||||
@@ -837,11 +953,20 @@ Item {
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
z: root.deleteConfirmOpen ? 20 : 0
|
||||
focus: true
|
||||
|
||||
Keys.priority: Keys.BeforeItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (root.deleteConfirmOpen) {
|
||||
if (deleteConfirm.handleKey(event)) event.accepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Delete) {
|
||||
root.requestDeleteSelected()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
if (root.filterText) root.setFilter("")
|
||||
else root.cancel()
|
||||
event.accepted = true
|
||||
@@ -875,6 +1000,25 @@ Item {
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDialog {
|
||||
id: deleteConfirm
|
||||
|
||||
anchors.fill: parent
|
||||
opened: root.deleteConfirmOpen
|
||||
z: 10
|
||||
message: "Do you want to uninstall " + ((root.deleteTarget && root.deleteTarget.label) || "") + "?"
|
||||
confirmText: "Uninstall"
|
||||
background: root.background
|
||||
foreground: root.foreground
|
||||
scrim: root.scrim
|
||||
selectedBackground: root.selectedBackground
|
||||
selectedText: root.selectedText
|
||||
fontFamily: root.fontFamily
|
||||
cornerRadius: root.cornerRadius
|
||||
onCanceled: root.cancelDelete()
|
||||
onConfirmed: root.confirmDelete()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -944,6 +1088,8 @@ Item {
|
||||
required property string kind
|
||||
required property string icon
|
||||
required property string iconFont
|
||||
required property string appIcon
|
||||
required property string appId
|
||||
required property string label
|
||||
required property string target
|
||||
required property string detail
|
||||
@@ -952,7 +1098,8 @@ Item {
|
||||
required property int childCount
|
||||
|
||||
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
|
||||
readonly property bool hasIcon: row.icon.length > 0
|
||||
readonly property bool isApp: row.kind === "app"
|
||||
readonly property bool hasIcon: row.icon.length > 0 || row.isApp
|
||||
|
||||
width: ListView.view.width
|
||||
height: root.rowHeightForDetail(row.detail)
|
||||
@@ -973,7 +1120,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
visible: row.hasIcon
|
||||
visible: row.hasIcon && !row.isApp
|
||||
text: row.icon
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
font.family: row.iconFont.length > 0 ? row.iconFont : root.fontFamily
|
||||
@@ -986,6 +1133,23 @@ Item {
|
||||
y: contentColumn.y + labelText.y + (labelText.height - height) / 2
|
||||
}
|
||||
|
||||
Image {
|
||||
id: appIconImage
|
||||
visible: row.isApp
|
||||
width: Style.font.iconLarge
|
||||
height: Style.font.iconLarge
|
||||
fillMode: Image.PreserveAspectFit
|
||||
// Decode at physical pixels — a logical-size decode leaves
|
||||
// PNG icons upscaled and blurry on HiDPI displays.
|
||||
sourceSize.width: width * Screen.devicePixelRatio
|
||||
sourceSize.height: height * Screen.devicePixelRatio
|
||||
source: row.isApp && root.appLibrary ? root.appLibrary.iconSource(row.appIcon) : ""
|
||||
asynchronous: true
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.rowReservedBorderLeft + Style.space(8) + (Style.space(36) - width) / 2
|
||||
y: contentColumn.y + labelText.y + (labelText.height - height) / 2
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.left: row.hasIcon ? iconText.right : parent.left
|
||||
|
||||
@@ -261,6 +261,8 @@ function displayRow(items, itemOrder, checkedResults, entry, detail, score, sect
|
||||
kind: entry.kind,
|
||||
icon: entry.icon,
|
||||
iconFont: entry.iconFont || "",
|
||||
appIcon: entry.appIcon || "",
|
||||
appId: entry.appId || "",
|
||||
label: labelFor(entry, checkedResults),
|
||||
target: target,
|
||||
detail: detail || "",
|
||||
|
||||
Reference in New Issue
Block a user