From 6ac2a305f7df1bd393b4e9707eb8df1ea60b5070 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 21 Jan 2026 14:08:40 -0500 Subject: [PATCH] launcher v2: sort order preference for plugin results --- quickshell/Common/SettingsData.qml | 22 ++ quickshell/Common/settings/SettingsSpec.js | 3 +- .../Modals/DankLauncherV2/Controller.qml | 136 +++++++++--- quickshell/Modules/Settings/LauncherTab.qml | 203 ++++++++++++------ 4 files changed, 265 insertions(+), 99 deletions(-) diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index ad381cb9..3f0b9509 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -96,6 +96,28 @@ Singleton { saveSettings(); } + property var launcherPluginOrder: [] + onLauncherPluginOrderChanged: saveSettings() + + function setLauncherPluginOrder(order) { + launcherPluginOrder = order; + } + + function getOrderedLauncherPlugins(allPlugins) { + if (!launcherPluginOrder || launcherPluginOrder.length === 0) + return allPlugins; + const orderMap = {}; + for (let i = 0; i < launcherPluginOrder.length; i++) + orderMap[launcherPluginOrder[i]] = i; + return allPlugins.slice().sort((a, b) => { + const aOrder = orderMap[a.id] ?? 9999; + const bOrder = orderMap[b.id] ?? 9999; + if (aOrder !== bOrder) + return aOrder - bOrder; + return a.name.localeCompare(b.name); + }); + } + property alias dankBarLeftWidgetsModel: leftWidgetsModel property alias dankBarCenterWidgetsModel: centerWidgetsModel property alias dankBarRightWidgetsModel: rightWidgetsModel diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index b25e4d5a..149e9c80 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -416,7 +416,8 @@ var SPEC = { desktopWidgetGroups: { def: [] }, builtInPluginSettings: { def: {} }, - launcherPluginVisibility: { def: {} } + launcherPluginVisibility: { def: {} }, + launcherPluginOrder: { def: [] } }; function getValidKeys() { diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index 63294e18..bf5698ac 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -431,35 +431,29 @@ Item { if (searchMode === "all") { if (searchQuery) { - var allPluginIds = getVisibleLauncherPluginIds(); - for (var i = 0; i < allPluginIds.length; i++) { - var pluginId = allPluginIds[i]; - var pItems = getPluginItems(pluginId, searchQuery); - allItems = allItems.concat(pItems); - } - - var allBuiltInIds = getVisibleBuiltInLauncherIds(); - for (var i = 0; i < allBuiltInIds.length; i++) { - var pluginId = allBuiltInIds[i]; - var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery); - for (var j = 0; j < blItems.length; j++) { - allItems.push(transformBuiltInLauncherItem(blItems[j], pluginId)); + var allPluginsOrdered = getAllVisiblePluginsOrdered(); + for (var i = 0; i < allPluginsOrdered.length; i++) { + var plugin = allPluginsOrdered[i]; + if (plugin.isBuiltIn) { + var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); + for (var j = 0; j < blItems.length; j++) + allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id)); + } else { + var pItems = getPluginItems(plugin.id, searchQuery); + allItems = allItems.concat(pItems); } } } else { - var emptyTriggerPlugins = getEmptyTriggerPlugins(); - for (var i = 0; i < emptyTriggerPlugins.length; i++) { - var pluginId = emptyTriggerPlugins[i]; - var pItems = getPluginItems(pluginId, searchQuery); - allItems = allItems.concat(pItems); - } - - var builtInLauncherPlugins = getBuiltInEmptyTriggerLaunchers(); - for (var i = 0; i < builtInLauncherPlugins.length; i++) { - var pluginId = builtInLauncherPlugins[i]; - var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery); - for (var j = 0; j < blItems.length; j++) { - allItems.push(transformBuiltInLauncherItem(blItems[j], pluginId)); + var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered(); + for (var i = 0; i < emptyTriggerOrdered.length; i++) { + var plugin = emptyTriggerOrdered[i]; + if (plugin.isBuiltIn) { + var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); + for (var j = 0; j < blItems.length; j++) + allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id)); + } else { + var pItems = getPluginItems(plugin.id, searchQuery); + allItems = allItems.concat(pItems); } } @@ -859,9 +853,10 @@ Item { function getEmptyTriggerPlugins() { var plugins = PluginService.getPluginsWithEmptyTrigger(); - return plugins.filter(function (pluginId) { + var visible = plugins.filter(function (pluginId) { return SettingsData.getPluginAllowWithoutTrigger(pluginId); }); + return sortPluginIdsByOrder(visible); } function getAllLauncherPluginIds() { @@ -871,9 +866,10 @@ Item { function getVisibleLauncherPluginIds() { var launchers = PluginService.getLauncherPlugins(); - return Object.keys(launchers).filter(function (pluginId) { + var visible = Object.keys(launchers).filter(function (pluginId) { return SettingsData.getPluginAllowWithoutTrigger(pluginId); }); + return sortPluginIdsByOrder(visible); } function getAllBuiltInLauncherIds() { @@ -883,9 +879,88 @@ Item { function getVisibleBuiltInLauncherIds() { var launchers = AppSearchService.getBuiltInLauncherPlugins(); - return Object.keys(launchers).filter(function (pluginId) { + var visible = Object.keys(launchers).filter(function (pluginId) { return SettingsData.getPluginAllowWithoutTrigger(pluginId); }); + return sortPluginIdsByOrder(visible); + } + + function sortPluginIdsByOrder(pluginIds) { + var order = SettingsData.launcherPluginOrder || []; + if (order.length === 0) + return pluginIds; + var orderMap = {}; + for (var i = 0; i < order.length; i++) + orderMap[order[i]] = i; + return pluginIds.slice().sort(function (a, b) { + var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999; + var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999; + return aOrder - bOrder; + }); + } + + function getAllVisiblePluginsOrdered() { + var thirdPartyLaunchers = PluginService.getLauncherPlugins() || {}; + var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins() || {}; + var all = []; + for (var id in thirdPartyLaunchers) { + if (SettingsData.getPluginAllowWithoutTrigger(id)) + all.push({ + id: id, + isBuiltIn: false + }); + } + for (var id in builtInLaunchers) { + if (SettingsData.getPluginAllowWithoutTrigger(id)) + all.push({ + id: id, + isBuiltIn: true + }); + } + var order = SettingsData.launcherPluginOrder || []; + if (order.length === 0) + return all; + var orderMap = {}; + for (var i = 0; i < order.length; i++) + orderMap[order[i]] = i; + return all.sort(function (a, b) { + var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999; + var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999; + return aOrder - bOrder; + }); + } + + function getEmptyTriggerPluginsOrdered() { + var thirdParty = PluginService.getPluginsWithEmptyTrigger() || []; + var builtIn = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger() || []; + var all = []; + for (var i = 0; i < thirdParty.length; i++) { + var id = thirdParty[i]; + if (SettingsData.getPluginAllowWithoutTrigger(id)) + all.push({ + id: id, + isBuiltIn: false + }); + } + for (var i = 0; i < builtIn.length; i++) { + var id = builtIn[i]; + if (SettingsData.getPluginAllowWithoutTrigger(id)) + all.push({ + id: id, + isBuiltIn: true + }); + } + var order = SettingsData.launcherPluginOrder || []; + if (order.length === 0) + return all; + var orderMap = {}; + for (var i = 0; i < order.length; i++) + orderMap[order[i]] = i; + return all.sort(function (a, b) { + var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999; + var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999; + return aOrder - bOrder; + }); } function getPluginBrowseItems() { @@ -948,9 +1023,10 @@ Item { function getBuiltInEmptyTriggerLaunchers() { var plugins = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger(); - return plugins.filter(function (pluginId) { + var visible = plugins.filter(function (pluginId) { return SettingsData.getPluginAllowWithoutTrigger(pluginId); }); + return sortPluginIdsByOrder(visible); } function getPluginItems(pluginId, query) { diff --git a/quickshell/Modules/Settings/LauncherTab.qml b/quickshell/Modules/Settings/LauncherTab.qml index a7c033c0..5ab7566f 100644 --- a/quickshell/Modules/Settings/LauncherTab.qml +++ b/quickshell/Modules/Settings/LauncherTab.qml @@ -581,6 +581,7 @@ Item { property var allLauncherPlugins: { SettingsData.launcherPluginVisibility; + SettingsData.launcherPluginOrder; var plugins = []; var builtIn = AppSearchService.getBuiltInLauncherPlugins() || {}; for (var pluginId in builtIn) { @@ -607,109 +608,175 @@ Item { trigger: PluginService.getPluginTrigger(pluginId) || "" }); } - return plugins.sort((a, b) => a.name.localeCompare(b.name)); + return SettingsData.getOrderedLauncherPlugins(plugins); + } + + function reorderPlugin(fromIndex, toIndex) { + if (fromIndex === toIndex) + return; + var currentOrder = allLauncherPlugins.map(p => p.id); + var item = currentOrder.splice(fromIndex, 1)[0]; + currentOrder.splice(toIndex, 0, item); + SettingsData.setLauncherPluginOrder(currentOrder); } StyledText { width: parent.width - text: I18n.tr("Control which plugins appear in 'All' mode without requiring a trigger prefix. Disabled plugins will only show when using their trigger.") + text: I18n.tr("Control which plugins appear in 'All' mode without requiring a trigger prefix. Drag to reorder.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap } Column { + id: pluginVisibilityColumn width: parent.width spacing: Theme.spacingS Repeater { model: pluginVisibilityCard.allLauncherPlugins - delegate: Rectangle { - id: visibilityDelegate + delegate: Item { + id: visibilityDelegateItem required property var modelData required property int index - width: parent.width + property bool held: pluginDragArea.pressed + property real originalY: y + + width: pluginVisibilityColumn.width height: 52 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + z: held ? 2 : 1 - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM + Rectangle { + id: visibilityDelegate + width: parent.width + height: 52 + radius: Theme.cornerRadius + color: visibilityDelegateItem.held ? Theme.surfaceHover : Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) - Item { - width: Theme.iconSize - height: Theme.iconSize + Row { + anchors.left: parent.left + anchors.leftMargin: 28 anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM - DankIcon { - anchors.centerIn: parent - visible: visibilityDelegate.modelData.iconType !== "unicode" - name: visibilityDelegate.modelData.icon - size: Theme.iconSize - color: Theme.primary + Item { + width: Theme.iconSize + height: Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + visible: visibilityDelegateItem.modelData.iconType !== "unicode" + name: visibilityDelegateItem.modelData.icon + size: Theme.iconSize + color: Theme.primary + } + + StyledText { + anchors.centerIn: parent + visible: visibilityDelegateItem.modelData.iconType === "unicode" + text: visibilityDelegateItem.modelData.icon + font.pixelSize: Theme.iconSize + color: Theme.primary + } } - StyledText { - anchors.centerIn: parent - visible: visibilityDelegate.modelData.iconType === "unicode" - text: visibilityDelegate.modelData.icon - font.pixelSize: Theme.iconSize - color: Theme.primary + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Row { + spacing: Theme.spacingS + + StyledText { + text: visibilityDelegateItem.modelData.name + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + Rectangle { + visible: visibilityDelegateItem.modelData.isBuiltIn + width: dmsBadgeLabel.implicitWidth + Theme.spacingS + height: 16 + radius: 8 + color: Theme.primaryContainer + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: dmsBadgeLabel + anchors.centerIn: parent + text: "DMS" + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.primaryText + } + } + } + + StyledText { + text: visibilityDelegateItem.modelData.trigger ? I18n.tr("Trigger: %1").arg(visibilityDelegateItem.modelData.trigger) : I18n.tr("No trigger") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } } } - Column { + DankToggle { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter - spacing: 2 - - Row { - spacing: Theme.spacingS - - StyledText { - text: visibilityDelegate.modelData.name - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - } - - Rectangle { - visible: visibilityDelegate.modelData.isBuiltIn - width: dmsLabel.implicitWidth + Theme.spacingS - height: 16 - radius: 8 - color: Theme.primaryContainer - anchors.verticalCenter: parent.verticalCenter - - StyledText { - id: dmsLabel - anchors.centerIn: parent - text: "DMS" - font.pixelSize: Theme.fontSizeSmall - 2 - color: Theme.primaryText - } - } - } - - StyledText { - text: visibilityDelegate.modelData.trigger ? I18n.tr("Trigger: %1").arg(visibilityDelegate.modelData.trigger) : I18n.tr("No trigger") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText + checked: SettingsData.getPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id) + onToggled: function (isChecked) { + SettingsData.setPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id, isChecked); } } } - DankToggle { - id: visibilityToggle - anchors.right: parent.right - anchors.rightMargin: Theme.spacingM + MouseArea { + id: pluginDragArea + anchors.left: parent.left + anchors.top: parent.top + width: 28 + height: parent.height + hoverEnabled: true + cursorShape: Qt.SizeVerCursor + drag.target: visibilityDelegateItem.held ? visibilityDelegateItem : undefined + drag.axis: Drag.YAxis + preventStealing: true + + onPressed: { + visibilityDelegateItem.originalY = visibilityDelegateItem.y; + } + + onReleased: { + if (!drag.active) { + visibilityDelegateItem.y = visibilityDelegateItem.originalY; + return; + } + const spacing = Theme.spacingS; + const itemH = visibilityDelegateItem.height + spacing; + var newIndex = Math.round(visibilityDelegateItem.y / itemH); + newIndex = Math.max(0, Math.min(newIndex, pluginVisibilityCard.allLauncherPlugins.length - 1)); + pluginVisibilityCard.reorderPlugin(visibilityDelegateItem.index, newIndex); + visibilityDelegateItem.y = visibilityDelegateItem.originalY; + } + } + + DankIcon { + x: Theme.spacingXS anchors.verticalCenter: parent.verticalCenter - checked: SettingsData.getPluginAllowWithoutTrigger(visibilityDelegate.modelData.id) - onToggled: function (isChecked) { - SettingsData.setPluginAllowWithoutTrigger(visibilityDelegate.modelData.id, isChecked); + name: "drag_indicator" + size: 18 + color: Theme.outline + opacity: pluginDragArea.containsMouse || pluginDragArea.pressed ? 1 : 0.5 + } + + Behavior on y { + enabled: !pluginDragArea.pressed && !pluginDragArea.drag.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing } } }