From 39220704883f2766facb541623fcdbb54e3ea907 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 21 Jan 2026 11:38:48 -0500 Subject: [PATCH] launcher v2: meta improvements - Allow disabling each plugin from "all" mode - add IPCs for toggling specific modes - niri: overview respect size & default to apps mode - fix unicode icon handling --- quickshell/Common/SettingsData.qml | 17 ++ quickshell/Common/settings/SettingsSpec.js | 3 +- quickshell/DMSShellIPC.qml | 14 ++ .../Modals/DankLauncherV2/Controller.qml | 36 +++- .../DankLauncherV2/DankLauncherV2Modal.qml | 31 +++- quickshell/Modals/DankLauncherV2/GridItem.qml | 8 + .../Modals/DankLauncherV2/ResultItem.qml | 8 + quickshell/Modals/DankLauncherV2/TileItem.qml | 10 +- quickshell/Modules/Settings/LauncherTab.qml | 154 ++++++++++++++++++ .../WorkspaceOverlays/NiriOverviewOverlay.qml | 13 +- quickshell/Services/PopoutService.qml | 33 +++- 11 files changed, 312 insertions(+), 15 deletions(-) diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 2bb8bbc2..01c284f9 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -79,6 +79,23 @@ Singleton { saveSettings(); } + property var launcherPluginVisibility: ({}) + + function getPluginAllowWithoutTrigger(pluginId) { + if (!launcherPluginVisibility[pluginId]) + return true; + return launcherPluginVisibility[pluginId].allowWithoutTrigger !== false; + } + + function setPluginAllowWithoutTrigger(pluginId, allow) { + const updated = JSON.parse(JSON.stringify(launcherPluginVisibility)); + if (!updated[pluginId]) + updated[pluginId] = {}; + updated[pluginId].allowWithoutTrigger = allow; + launcherPluginVisibility = updated; + saveSettings(); + } + 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 e9516713..b25e4d5a 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -415,7 +415,8 @@ var SPEC = { desktopWidgetGroups: { def: [] }, - builtInPluginSettings: { def: {} } + builtInPluginSettings: { def: {} }, + launcherPluginVisibility: { def: {} } }; function getValidKeys() { diff --git a/quickshell/DMSShellIPC.qml b/quickshell/DMSShellIPC.qml index 5a621df9..a0a3d09c 100644 --- a/quickshell/DMSShellIPC.qml +++ b/quickshell/DMSShellIPC.qml @@ -1041,6 +1041,20 @@ Item { return "LAUNCHER_TOGGLE_SUCCESS"; } + function openWith(mode: string): string { + if (!mode) + return "LAUNCHER_OPEN_FAILED: No mode specified"; + PopoutService.openSpotlightV2WithMode(mode); + return `LAUNCHER_OPEN_SUCCESS: ${mode}`; + } + + function toggleWith(mode: string): string { + if (!mode) + return "LAUNCHER_TOGGLE_FAILED: No mode specified"; + PopoutService.toggleSpotlightV2WithMode(mode); + return `LAUNCHER_TOGGLE_SUCCESS: ${mode}`; + } + function openQuery(query: string): string { PopoutService.openSpotlightV2WithQuery(query); return "LAUNCHER_OPEN_QUERY_SUCCESS"; diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index e3cb44d3..63294e18 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -431,14 +431,14 @@ Item { if (searchMode === "all") { if (searchQuery) { - var allPluginIds = getAllLauncherPluginIds(); + 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 = getAllBuiltInLauncherIds(); + var allBuiltInIds = getVisibleBuiltInLauncherIds(); for (var i = 0; i < allBuiltInIds.length; i++) { var pluginId = allBuiltInIds[i]; var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery); @@ -858,7 +858,10 @@ Item { } function getEmptyTriggerPlugins() { - return PluginService.getPluginsWithEmptyTrigger(); + var plugins = PluginService.getPluginsWithEmptyTrigger(); + return plugins.filter(function (pluginId) { + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + }); } function getAllLauncherPluginIds() { @@ -866,11 +869,25 @@ Item { return Object.keys(launchers); } + function getVisibleLauncherPluginIds() { + var launchers = PluginService.getLauncherPlugins(); + return Object.keys(launchers).filter(function (pluginId) { + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + }); + } + function getAllBuiltInLauncherIds() { var launchers = AppSearchService.getBuiltInLauncherPlugins(); return Object.keys(launchers); } + function getVisibleBuiltInLauncherIds() { + var launchers = AppSearchService.getBuiltInLauncherPlugins(); + return Object.keys(launchers).filter(function (pluginId) { + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + }); + } + function getPluginBrowseItems() { var items = []; @@ -930,7 +947,10 @@ Item { } function getBuiltInEmptyTriggerLaunchers() { - return AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger(); + var plugins = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger(); + return plugins.filter(function (pluginId) { + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + }); } function getPluginItems(pluginId, query) { @@ -947,12 +967,14 @@ Item { function detectIconType(iconName) { if (!iconName) return "material"; - if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0) - return "image"; + if (iconName.startsWith("unicode:")) + return "unicode"; if (iconName.startsWith("material:")) return "material"; if (iconName.startsWith("image:")) return "image"; + if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0) + return "image"; if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase())) return "image"; return "material"; @@ -961,6 +983,8 @@ Item { function stripIconPrefix(iconName) { if (!iconName) return "extension"; + if (iconName.startsWith("unicode:")) + return iconName.substring(8); if (iconName.startsWith("material:")) return iconName.substring(9); if (iconName.startsWith("image:")) diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index de0d3056..65f19fc9 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -53,7 +53,7 @@ Item { signal dialogClosed - function _initializeAndShow(query) { + function _initializeAndShow(query, mode) { contentVisible = true; spotlightContent.searchField.forceActiveFocus(); @@ -61,7 +61,8 @@ Item { spotlightContent.searchField.text = query; } if (spotlightContent.controller) { - spotlightContent.controller.searchMode = "all"; + var targetMode = mode || "all"; + spotlightContent.controller.searchMode = targetMode; spotlightContent.controller.activePluginId = ""; spotlightContent.controller.activePluginName = ""; spotlightContent.controller.pluginFilter = ""; @@ -136,6 +137,32 @@ Item { spotlightOpen ? hide() : show(); } + function showWithMode(mode) { + closeCleanupTimer.stop(); + isClosing = false; + openedFromOverview = false; + + var focusedScreen = CompositorService.getFocusedScreen(); + if (focusedScreen) + launcherWindow.screen = focusedScreen; + + spotlightOpen = true; + keyboardActive = true; + ModalManager.openModal(root); + if (useHyprlandFocusGrab) + focusGrab.active = true; + + _initializeAndShow("", mode); + } + + function toggleWithMode(mode) { + if (spotlightOpen) { + hide(); + } else { + showWithMode(mode); + } + } + Timer { id: closeCleanupTimer interval: Theme.expressiveDurations.expressiveFastSpatial + 50 diff --git a/quickshell/Modals/DankLauncherV2/GridItem.qml b/quickshell/Modals/DankLauncherV2/GridItem.qml index 7422a87f..9d40e549 100644 --- a/quickshell/Modals/DankLauncherV2/GridItem.qml +++ b/quickshell/Modals/DankLauncherV2/GridItem.qml @@ -52,6 +52,14 @@ Rectangle { color: root.isSelected ? Theme.primary : Theme.surfaceText } + StyledText { + anchors.centerIn: parent + visible: root.item?.iconType === "unicode" + text: root.item?.icon ?? "" + font.pixelSize: parent.iconSize * 0.7 + color: root.isSelected ? Theme.primary : Theme.surfaceText + } + Item { anchors.fill: parent visible: root.item?.iconType === "composite" diff --git a/quickshell/Modals/DankLauncherV2/ResultItem.qml b/quickshell/Modals/DankLauncherV2/ResultItem.qml index ce6df6a1..33cb8ea2 100644 --- a/quickshell/Modals/DankLauncherV2/ResultItem.qml +++ b/quickshell/Modals/DankLauncherV2/ResultItem.qml @@ -52,6 +52,14 @@ Rectangle { color: Theme.surfaceText } + StyledText { + anchors.centerIn: parent + visible: root.item?.iconType === "unicode" + text: root.item?.icon ?? "" + font.pixelSize: 24 + color: Theme.surfaceText + } + Item { anchors.fill: parent visible: root.item?.iconType === "composite" diff --git a/quickshell/Modals/DankLauncherV2/TileItem.qml b/quickshell/Modals/DankLauncherV2/TileItem.qml index ca8d61fa..bcd2c073 100644 --- a/quickshell/Modals/DankLauncherV2/TileItem.qml +++ b/quickshell/Modals/DankLauncherV2/TileItem.qml @@ -76,12 +76,20 @@ Rectangle { DankIcon { anchors.centerIn: parent - visible: !root.useImage && !root.useIconProvider + visible: !root.useImage && !root.useIconProvider && root.item?.iconType !== "unicode" name: root.item?.icon ?? "image" size: Math.min(parent.width, parent.height) * 0.4 color: Theme.surfaceVariantText } + StyledText { + anchors.centerIn: parent + visible: root.item?.iconType === "unicode" + text: root.item?.icon ?? "" + font.pixelSize: Math.min(parent.width, parent.height) * 0.4 + color: Theme.surfaceVariantText + } + Rectangle { anchors.left: parent.left anchors.right: parent.right diff --git a/quickshell/Modules/Settings/LauncherTab.qml b/quickshell/Modules/Settings/LauncherTab.qml index 8c36ca4d..a7c033c0 100644 --- a/quickshell/Modules/Settings/LauncherTab.qml +++ b/quickshell/Modules/Settings/LauncherTab.qml @@ -572,6 +572,160 @@ Item { } } + SettingsCard { + id: pluginVisibilityCard + width: parent.width + iconName: "filter_list" + title: I18n.tr("Plugin Visibility") + settingKey: "pluginVisibility" + + property var allLauncherPlugins: { + SettingsData.launcherPluginVisibility; + var plugins = []; + var builtIn = AppSearchService.getBuiltInLauncherPlugins() || {}; + for (var pluginId in builtIn) { + var plugin = builtIn[pluginId]; + plugins.push({ + id: pluginId, + name: plugin.name || pluginId, + icon: plugin.cornerIcon || "extension", + iconType: "material", + isBuiltIn: true, + trigger: AppSearchService.getBuiltInPluginTrigger(pluginId) || "" + }); + } + var thirdParty = PluginService.getLauncherPlugins() || {}; + for (var pluginId in thirdParty) { + var plugin = thirdParty[pluginId]; + var rawIcon = plugin.icon || "extension"; + plugins.push({ + id: pluginId, + name: plugin.name || pluginId, + icon: rawIcon.startsWith("material:") ? rawIcon.substring(9) : rawIcon.startsWith("unicode:") ? rawIcon.substring(8) : rawIcon, + iconType: rawIcon.startsWith("unicode:") ? "unicode" : "material", + isBuiltIn: false, + trigger: PluginService.getPluginTrigger(pluginId) || "" + }); + } + return plugins.sort((a, b) => a.name.localeCompare(b.name)); + } + + 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.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: pluginVisibilityCard.allLauncherPlugins + + delegate: Rectangle { + id: visibilityDelegate + required property var modelData + required property int index + + width: parent.width + height: 52 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Item { + width: Theme.iconSize + height: Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + visible: visibilityDelegate.modelData.iconType !== "unicode" + name: visibilityDelegate.modelData.icon + size: 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: 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 + } + } + } + + DankToggle { + id: visibilityToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.getPluginAllowWithoutTrigger(visibilityDelegate.modelData.id) + onToggled: function (isChecked) { + SettingsData.setPluginAllowWithoutTrigger(visibilityDelegate.modelData.id, isChecked); + } + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("No launcher plugins installed.") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: pluginVisibilityCard.allLauncherPlugins.length === 0 + } + } + } + SettingsCard { width: parent.width iconName: "search" diff --git a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml index 38451931..a18e77e5 100644 --- a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml +++ b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml @@ -122,8 +122,10 @@ Scope { onShouldShowSpotlightChanged: { if (shouldShowSpotlight) { - if (launcherContent?.controller) + if (launcherContent?.controller) { + launcherContent.controller.searchMode = "apps"; launcherContent.controller.performSearch(); + } return; } if (!isActiveScreen) @@ -198,8 +200,11 @@ Scope { id: spotlightContainer x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr) y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr) - width: Theme.px(500, overlayWindow.dpr) - height: Theme.px(600, overlayWindow.dpr) + + readonly property int baseWidth: SettingsData.dankLauncherV2Size === "medium" ? 720 : SettingsData.dankLauncherV2Size === "large" ? 860 : 620 + readonly property int baseHeight: SettingsData.dankLauncherV2Size === "medium" ? 720 : SettingsData.dankLauncherV2Size === "large" ? 860 : 600 + width: Math.min(baseWidth, overlayWindow.screen.width - 100) + height: Math.min(baseHeight, overlayWindow.screen.height - 100) readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen @@ -252,7 +257,7 @@ Scope { property bool isClosing: niriOverviewScope.isClosing function hide() { if (niriOverviewScope.searchActive) { - niriOverviewScope.hideAndReleaseKeyboard(); + niriOverviewScope.hideSpotlight(); return; } NiriService.toggleOverview(); diff --git a/quickshell/Services/PopoutService.qml b/quickshell/Services/PopoutService.qml index 860e2084..205df717 100644 --- a/quickshell/Services/PopoutService.qml +++ b/quickshell/Services/PopoutService.qml @@ -366,6 +366,7 @@ Singleton { property bool _spotlightV2WantsOpen: false property bool _spotlightV2WantsToggle: false property string _spotlightV2PendingQuery: "" + property string _spotlightV2PendingMode: "" function openSpotlightV2() { if (spotlightV2Modal) { @@ -388,6 +389,17 @@ Singleton { } } + function openSpotlightV2WithMode(mode: string) { + if (spotlightV2Modal) { + spotlightV2Modal.showWithMode(mode); + } else if (spotlightV2ModalLoader) { + _spotlightV2PendingMode = mode; + _spotlightV2WantsOpen = true; + _spotlightV2WantsToggle = false; + spotlightV2ModalLoader.active = true; + } + } + function closeSpotlightV2() { spotlightV2Modal?.hide(); } @@ -402,12 +414,26 @@ Singleton { } } + function toggleSpotlightV2WithMode(mode: string) { + if (spotlightV2Modal) { + spotlightV2Modal.toggleWithMode(mode); + } else if (spotlightV2ModalLoader) { + _spotlightV2PendingMode = mode; + _spotlightV2WantsToggle = true; + _spotlightV2WantsOpen = false; + spotlightV2ModalLoader.active = true; + } + } + function _onSpotlightV2ModalLoaded() { if (_spotlightV2WantsOpen) { _spotlightV2WantsOpen = false; if (_spotlightV2PendingQuery) { spotlightV2Modal?.showWithQuery(_spotlightV2PendingQuery); _spotlightV2PendingQuery = ""; + } else if (_spotlightV2PendingMode) { + spotlightV2Modal?.showWithMode(_spotlightV2PendingMode); + _spotlightV2PendingMode = ""; } else { spotlightV2Modal?.show(); } @@ -415,7 +441,12 @@ Singleton { } if (_spotlightV2WantsToggle) { _spotlightV2WantsToggle = false; - spotlightV2Modal?.toggle(); + if (_spotlightV2PendingMode) { + spotlightV2Modal?.toggleWithMode(_spotlightV2PendingMode); + _spotlightV2PendingMode = ""; + } else { + spotlightV2Modal?.toggle(); + } } }