From fb9ec8e72111054edff34a380362e82a409d6600 Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 21 May 2026 01:05:56 -0400 Subject: [PATCH] feat: (Launcher/Spotlight): Updated w/New Settings & QOL features - New Spotlight toggle to show/hide chips, off by default - Updated blur effects on all launcher inputs and footers - Fixed previous queries resurfacing - Upated Spotlight keyboard navigation - Added functionality to show and shortcut to keybinds from the Launcher tab --- quickshell/Common/KeybindActions.js | 6 +- quickshell/Common/SettingsData.qml | 1 + quickshell/Common/settings/SettingsSpec.js | 1 + .../ClipboardLauncherPreview.qml | 29 +- .../Modals/DankLauncherV2/Controller.qml | 27 +- .../DankLauncherV2ModalConnected.qml | 2 + .../DankLauncherV2ModalSpotlight.qml | 8 +- .../DankLauncherV2ModalStandalone.qml | 13 +- .../Modals/DankLauncherV2/LauncherContent.qml | 44 +- .../DankLauncherV2/LauncherContextMenu.qml | 566 +++++++++++------- .../SpotlightLauncherContent.qml | 68 ++- .../DankLauncherV2/SpotlightResultsList.qml | 18 +- .../Modals/Settings/SettingsContent.qml | 1 + quickshell/Modals/Settings/SettingsModal.qml | 6 + quickshell/Modules/Settings/KeybindsTab.qml | 26 +- quickshell/Modules/Settings/LauncherTab.qml | 207 ++++++- quickshell/Services/AppSearchService.qml | 9 +- quickshell/Services/KeybindsService.qml | 18 + quickshell/Services/SessionService.qml | 4 + quickshell/Widgets/DankTextField.qml | 4 +- .../translations/settings_search_index.json | 137 ++++- 21 files changed, 867 insertions(+), 328 deletions(-) diff --git a/quickshell/Common/KeybindActions.js b/quickshell/Common/KeybindActions.js index e41be355..0d25ba28 100644 --- a/quickshell/Common/KeybindActions.js +++ b/quickshell/Common/KeybindActions.js @@ -8,9 +8,9 @@ const ACTION_TYPES = [ ]; const DMS_ACTIONS = [ - { id: "spawn dms ipc call spotlight toggle", label: "App Launcher: Toggle" }, - { id: "spawn dms ipc call spotlight open", label: "App Launcher: Open" }, - { id: "spawn dms ipc call spotlight close", label: "App Launcher: Close" }, + { id: "spawn dms ipc call spotlight toggle", label: "Default Launcher: Toggle" }, + { id: "spawn dms ipc call spotlight open", label: "Default Launcher: Open" }, + { id: "spawn dms ipc call spotlight close", label: "Default Launcher: Close" }, { id: "spawn dms ipc call spotlight-bar toggle", label: "Spotlight Bar: Toggle" }, { id: "spawn dms ipc call spotlight-bar open", label: "Spotlight Bar: Open" }, { id: "spawn dms ipc call spotlight-bar close", label: "Spotlight Bar: Close" }, diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 95c82791..6177d2f6 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -450,6 +450,7 @@ Singleton { property bool dankLauncherV2IncludeFoldersInAll: false property bool launcherUseOverlayLayer: false property string launcherStyle: "full" + property bool spotlightBarShowModeChips: false property string _legacyWeatherLocation: "New York, NY" property string _legacyWeatherCoordinates: "40.7128,-74.0060" diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index e61d65b1..b8e74c29 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -216,6 +216,7 @@ var SPEC = { dankLauncherV2IncludeFoldersInAll: { def: false }, launcherUseOverlayLayer: { def: false }, launcherStyle: { def: "full" }, + spotlightBarShowModeChips: { def: false }, useAutoLocation: { def: false }, weatherEnabled: { def: true }, diff --git a/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml b/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml index fe892d74..da2f9e49 100644 --- a/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml +++ b/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml @@ -10,10 +10,11 @@ Rectangle { property var entry: null property string cachedImageData: "" + property string cachedMimeType: "" property var _requestedEntryId: null readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/") - readonly property string sourceUrl: cachedImageData.length > 0 ? "data:" + (entry?.mimeType ?? "image/png") + ";base64," + cachedImageData : "" + readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? "")) radius: Math.max(6, Theme.cornerRadius - 2) clip: true @@ -24,8 +25,24 @@ Rectangle { onEntryChanged: reloadPreview() Component.onCompleted: reloadPreview() + function isImageMimeType(mimeType) { + return (mimeType || "").toString().toLowerCase().startsWith("image/"); + } + + function resolvedSourceUrl(data, mimeType) { + const rawData = (data || "").toString(); + if (rawData.length === 0) + return ""; + if (rawData.startsWith("data:")) + return rawData.startsWith("data:image/") ? rawData : ""; + if (!isImageMimeType(mimeType)) + return ""; + return "data:" + mimeType + ";base64," + rawData; + } + function reloadPreview() { cachedImageData = ""; + cachedMimeType = ""; if (!canLoadImage || !entry?.id) { _requestedEntryId = null; return; @@ -40,9 +57,13 @@ Rectangle { return; if (response.error) return; - const data = response.result?.data ?? ""; - if (data.length > 0) - cachedImageData = data; + const result = response.result ?? {}; + const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString(); + const data = (result.data ?? "").toString(); + if (data.length === 0 || !resolvedSourceUrl(data, mimeType)) + return; + cachedMimeType = mimeType; + cachedImageData = data; }); } diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index a8137f5b..a2b4de57 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -35,6 +35,7 @@ Item { property int gridColumns: SettingsData.appLauncherGridColumns property int viewModeVersion: 0 property string viewModeContext: "spotlight" + property bool forceLinearNavigation: false signal itemExecuted signal searchCompleted @@ -43,6 +44,10 @@ Item { signal viewModeChanged(string sectionId, string mode) signal searchQueryRequested(string query) + Ref { + service: AppSearchService + } + onActiveChanged: { if (!active) { SessionData.addLauncherHistory(searchQuery); @@ -87,15 +92,23 @@ Item { Connections { target: ClipboardService function onLauncherSearchReady(query) { - if (!active || !clipboardSearchEnabledInAll()) + if (!active) return; - if (searchMode !== "all") + + const clipboardBuiltInActive = activePluginId === "dms_clipboard_search"; + if (!clipboardBuiltInActive && !clipboardSearchEnabledInAll()) return; + if (!clipboardBuiltInActive && searchMode !== "all") + return; + const trimmed = (searchQuery || "").trim(); if (trimmed.length < 2 && query.length > 0) return; - if (query !== trimmed) + const triggerMatch = detectTrigger(trimmed); + const effectiveQuery = clipboardBuiltInActive && triggerMatch.pluginId === "dms_clipboard_search" ? triggerMatch.query : trimmed; + if (query !== effectiveQuery) return; + searchDebounce.restart(); } } @@ -1711,7 +1724,9 @@ Item { function selectNext() { keyboardNavigationActive = true; _cancelPendingSelectionReset(); - var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); + var newIndex = forceLinearNavigation ? Nav.findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1) : Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); + if (newIndex === -1) + newIndex = selectedFlatIndex; if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; updateSelectedItem(); @@ -1721,7 +1736,9 @@ Item { function selectPrevious() { keyboardNavigationActive = true; _cancelPendingSelectionReset(); - var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); + var newIndex = forceLinearNavigation ? Nav.findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1) : Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); + if (newIndex === -1) + newIndex = selectedFlatIndex; if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; updateSelectedItem(); diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml index 3671b1ab..bc7d9c85 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml @@ -388,6 +388,7 @@ Item { if (!spotlightContent) return; contentVisible = true; + spotlightContent.closeTransientUi?.(); // NOTE: forceActiveFocus() is deliberately NOT called here. // It is deferred to after animation starts to avoid compositor IPC stalls. @@ -480,6 +481,7 @@ Item { function hide() { if (!spotlightOpen) return; + spotlightContent?.closeTransientUi?.(); openedFromOverview = false; isClosing = true; // For directional effects, defer contentVisible=false so content stays rendered during exit slide diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml index 07c730d4..16b2bdcb 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml @@ -47,9 +47,9 @@ Item { } } - readonly property int _openDuration: 80 - readonly property int _closeDuration: 70 - readonly property int _motionDuration: 90 + readonly property int _openDuration: 50 + readonly property int _closeDuration: 40 + readonly property int _motionDuration: 60 // Connected frame mode clamps the centered surface inside frame insets. readonly property bool frameConnected: CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen) @@ -142,6 +142,7 @@ Item { if (!spotlightContent) return; contentVisible = true; + spotlightContent.closeTransientUi?.(); const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : ""); const targetMode = mode || SessionData.launcherLastMode || "all"; @@ -202,6 +203,7 @@ Item { function hide() { if (!spotlightOpen) return; + spotlightContent?.closeTransientUi?.(); openedFromOverview = false; isClosing = true; contentVisible = false; diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml index 7a40abc9..0ea1e276 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml @@ -133,6 +133,7 @@ Item { if (!spotlightContent) return; contentVisible = true; + spotlightContent.closeTransientUi?.(); spotlightContent.searchField.forceActiveFocus(); var targetQuery = ""; @@ -211,6 +212,7 @@ Item { function hide() { if (!spotlightOpen) return; + spotlightContent?.closeTransientUi?.(); openedFromOverview = false; isClosing = true; contentVisible = false; @@ -368,7 +370,7 @@ Item { WindowBlur { targetWindow: launcherWindow readonly property real s: Math.min(1, modalContainer.publishedScale) - readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2)) + readonly property real op: Math.max(0, Math.min(1, (modalContainer.publishedOpacity - 0.06) * 2)) blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5 blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5 blurWidth: contentVisible ? modalContainer.width * s * op : 0 @@ -447,6 +449,7 @@ Item { property bool _renderActive: contentVisible property real publishedScale: contentVisible ? 1 : 0.96 + property real publishedOpacity: contentVisible ? 1 : 0 opacity: contentVisible ? 1 : 0 scale: contentVisible ? 1 : 0.96 @@ -478,6 +481,14 @@ Item { } } + Behavior on publishedOpacity { + NumberAnimation { + easing.type: Easing.BezierSpline + duration: Theme.modalAnimationDuration + easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + } + } + Connections { target: root function onContentVisibleChanged() { diff --git a/quickshell/Modals/DankLauncherV2/LauncherContent.qml b/quickshell/Modals/DankLauncherV2/LauncherContent.qml index f621f8c4..9549fee8 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContent.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContent.qml @@ -21,6 +21,17 @@ FocusScope { property bool editMode: false property var editingApp: null property string editAppId: "" + readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers + readonly property real _launcherFieldAlpha: { + if (Theme.transparentBlurLayers) + return 0.28; + if (Theme.blurForegroundLayers) + return Math.max(Theme.popupTransparency, 0.62); + return Theme.popupTransparency; + } + readonly property color _launcherSearchFieldColor: Theme.withAlpha(Theme.surfaceContainerHigh, _launcherFieldAlpha) + readonly property color _launcherSearchBorderColor: Theme.withAlpha(Theme.outline, _blurActive ? 0.16 : Theme.layerOutlineOpacity) + readonly property color _launcherSearchFocusedBorderColor: Theme.withAlpha(Theme.primary, _blurActive ? 0.72 : 1.0) function resetScroll() { resultsList.resetScroll(); @@ -30,6 +41,12 @@ FocusScope { searchField.forceActiveFocus(); } + function closeTransientUi() { + contextMenu.hide(); + actionPanel.hide(); + root.enabled = true; + } + function openEditMode(app) { if (!app) return; @@ -111,6 +128,21 @@ FocusScope { } } + Connections { + target: root.parentModal + ignoreUnknownSignals: true + + function onSpotlightOpenChanged() { + if (!root.parentModal?.spotlightOpen) + root.closeTransientUi(); + } + + function onContentVisibleChanged() { + if (!root.parentModal?.contentVisible) + root.closeTransientUi(); + } + } + Keys.onPressed: event => { if (editMode) { if (event.key === Qt.Key_Escape) { @@ -284,7 +316,7 @@ FocusScope { anchors.bottom: parent.bottom anchors.leftMargin: root.parentModal?.borderWidth ?? 1 anchors.rightMargin: root.parentModal?.borderWidth ?? 1 - anchors.bottomMargin: _connectedBottomEmerge ? Theme.spacingS : (root.parentModal?.borderWidth ?? 1) + anchors.bottomMargin: _connectedBottomEmerge ? 0 : (root.parentModal?.borderWidth ?? 1) height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0 visible: showFooter clip: true @@ -293,7 +325,7 @@ FocusScope { anchors.fill: parent anchors.topMargin: -Theme.cornerRadius // In connected mode the launcher provides the surface so update the toolbar for arcs - visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) + visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) && !root._blurActive color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) radius: Theme.cornerRadius } @@ -458,9 +490,11 @@ FocusScope { id: searchField width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0) cornerRadius: Theme.cornerRadius - backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - normalBorderColor: Theme.outlineMedium - focusedBorderColor: Theme.primary + backgroundColor: root._launcherSearchFieldColor + normalBorderColor: root._launcherSearchBorderColor + focusedBorderColor: root._launcherSearchFocusedBorderColor + borderWidth: 1 + focusedBorderWidth: 2 leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search" leftIconSize: Theme.iconSize leftIconColor: Theme.surfaceVariantText diff --git a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml index 7779c1e0..330b36f0 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml @@ -1,35 +1,72 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Controls +import Quickshell +import Quickshell.Wayland import qs.Common import qs.Services import qs.Widgets -Popup { +Item { id: root + visible: false + width: 0 + height: 0 + property var item: null property var controller: null property var searchField: null property var parentHandler: null property bool allowEditActions: true + property real menuMargin: 8 + property var targetScreen: null + property real anchorX: 0 + property real anchorY: 0 + property bool openState: false + property bool renderActive: false + readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled + + readonly property real minMenuWidth: 180 + readonly property real maxMenuWidth: Math.max(0, (targetScreen?.width ?? 500) - menuMargin * 2) + readonly property real maxMenuHeight: Math.max(0, (targetScreen?.height ?? 600) - menuMargin * 2) + readonly property string longestMenuText: { + let longest = ""; + for (let i = 0; i < menuItems.length; i++) { + const text = menuItems[i].text || ""; + if (text.length > longest.length) + longest = text; + } + return longest; + } + readonly property real naturalMenuWidth: Math.max(minMenuWidth, menuTextMetrics.width + Theme.iconSize + Theme.spacingS * 5) + readonly property real effectiveMenuWidth: Math.max(0, Math.min(maxMenuWidth, naturalMenuWidth)) + readonly property real naturalMenuHeight: menuItemsHeight() + Theme.spacingS * 2 + readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight) + readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5 signal hideRequested signal editAppRequested(var app) + TextMetrics { + id: menuTextMetrics + text: root.longestMenuText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + function hasContextMenuActions(spotlightItem) { if (!spotlightItem) return false; if (spotlightItem.type === "app") return true; if (spotlightItem.type === "plugin" && spotlightItem.pluginId) { - var instance = PluginService.pluginInstances[spotlightItem.pluginId]; + const instance = PluginService.pluginInstances[spotlightItem.pluginId]; if (!instance) return false; if (typeof instance.getContextMenuActions !== "function") return false; - var actions = instance.getContextMenuActions(spotlightItem.data); + const actions = instance.getContextMenuActions(spotlightItem.data); return Array.isArray(actions) && actions.length > 0; } if (spotlightItem.actions && spotlightItem.actions.length > 0) @@ -54,13 +91,13 @@ Popup { if (!isPluginItem || !item?.pluginId) return []; - var instance = PluginService.pluginInstances[item.pluginId]; + const instance = PluginService.pluginInstances[item.pluginId]; if (!instance) return []; if (typeof instance.getContextMenuActions !== "function") return []; - var actions = instance.getContextMenuActions(item.data); + const actions = instance.getContextMenuActions(item.data); if (!Array.isArray(actions)) return []; @@ -68,8 +105,8 @@ Popup { } function executePluginAction(actionOrObj) { - var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action; - var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher; + const actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action; + const closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher; if (typeof actionFunc === "function") actionFunc(); @@ -90,12 +127,12 @@ Popup { } readonly property var menuItems: { - var items = []; + const items = []; if (isPluginItem) { - var pluginActions = getPluginContextMenuActions(); - for (var i = 0; i < pluginActions.length; i++) { - var act = pluginActions[i]; + const pluginActions = getPluginContextMenuActions(); + for (let i = 0; i < pluginActions.length; i++) { + const act = pluginActions[i]; items.push({ type: "item", icon: act.icon || "play_arrow", @@ -107,8 +144,8 @@ Popup { } if (item?.type !== "app" && item?.actions && item.actions.length > 0) { - for (var i = 0; i < item.actions.length; i++) { - var genericAct = item.actions[i]; + for (let i = 0; i < item.actions.length; i++) { + const genericAct = item.actions[i]; items.push({ type: "item", icon: genericAct.icon || "play_arrow", @@ -149,8 +186,8 @@ Popup { items.push({ type: "separator" }); - for (var i = 0; i < item.actions.length; i++) { - var act = item.actions[i]; + for (let i = 0; i < item.actions.length; i++) { + const act = item.actions[i]; items.push({ type: "item", icon: act.icon || "play_arrow", @@ -183,43 +220,52 @@ Popup { return items; } + function menuItemsHeight() { + let h = 0; + for (let i = 0; i < menuItems.length; i++) { + h += menuItems[i].type === "separator" ? 5 : 32; + } + if (menuItems.length > 1) + h += menuItems.length - 1; + return h; + } + function show(x, y, spotlightItem, fromKeyboard) { if (!spotlightItem?.data) return; + item = spotlightItem; selectedMenuIndex = fromKeyboard ? 0 : -1; keyboardNavigation = fromKeyboard; + const modal = parentHandler?.parentModal ?? null; + const screenRef = modal?.effectiveScreen ?? parentHandler?.Window?.window?.screen ?? searchField?.Window?.window?.screen ?? null; + const screenX = screenRef?.x || 0; + const screenY = screenRef?.y || 0; + const screenRelativeX = modal ? ((modal.alignedX ?? 0) + x) : ((parentHandler ? parentHandler.mapToGlobal(x, y).x : x) - screenX); + const screenRelativeY = modal ? ((modal.alignedY ?? 0) + y) : ((parentHandler ? parentHandler.mapToGlobal(x, y).y : y) - screenY); + + targetScreen = screenRef; + anchorX = screenRelativeX + 4; + anchorY = screenRelativeY + 4; + renderActive = true; + openState = true; + if (parentHandler) parentHandler.enabled = false; Qt.callLater(() => { - var parentW = parent?.width ?? 500; - var parentH = parent?.height ?? 600; - var menuW = width > 0 ? width : 200; - var menuH = height > 0 ? height : 200; - var margin = 8; - - var posX = x + 4; - var posY = y + 4; - - if (posX + menuW > parentW - margin) { - posX = Math.max(margin, parentW - menuW - margin); - } - if (posY + menuH > parentH - margin) { - posY = Math.max(margin, parentH - menuH - margin); - } - - root.x = posX; - root.y = posY; - open(); + menuFlickable.contentY = 0; + keyboardHandler.forceActiveFocus(); + ensureSelectedVisible(); }); } function hide() { - if (parentHandler) - parentHandler.enabled = true; - close(); + if (!renderActive) + return; + openState = false; + hideRequested(); } function togglePin() { @@ -286,8 +332,8 @@ Popup { property bool keyboardNavigation: false readonly property int visibleItemCount: { - var count = 0; - for (var i = 0; i < menuItems.length; i++) { + let count = 0; + for (let i = 0; i < menuItems.length; i++) { if (menuItems[i].type === "item") count++; } @@ -295,22 +341,62 @@ Popup { } function selectNext() { - if (visibleItemCount > 0) + if (visibleItemCount > 0) { + keyboardNavigation = true; selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount; + ensureSelectedVisible(); + } } function selectPrevious() { - if (visibleItemCount > 0) + if (visibleItemCount > 0) { + keyboardNavigation = true; selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount; + ensureSelectedVisible(); + } + } + + function selectedDelegateIndex() { + let itemIndex = 0; + for (let i = 0; i < menuItems.length; i++) { + if (menuItems[i].type !== "item") + continue; + if (itemIndex === selectedMenuIndex) + return i; + itemIndex++; + } + return -1; + } + + function ensureSelectedVisible() { + Qt.callLater(() => { + if (!menuFlickable || !menuRepeater) + return; + const delegateIndex = selectedDelegateIndex(); + if (delegateIndex < 0) + return; + const delegate = menuRepeater.itemAt(delegateIndex); + if (!delegate) + return; + const top = delegate.y; + const bottom = top + delegate.height; + const viewTop = menuFlickable.contentY; + const viewBottom = viewTop + menuFlickable.height; + if (top < viewTop) { + menuFlickable.contentY = Math.max(0, top); + } else if (bottom > viewBottom) { + menuFlickable.contentY = Math.min(Math.max(0, menuFlickable.contentHeight - menuFlickable.height), bottom - menuFlickable.height); + } + }); } function activateSelected() { - var itemIndex = 0; - for (var i = 0; i < menuItems.length; i++) { + let itemIndex = 0; + for (let i = 0; i < menuItems.length; i++) { if (menuItems[i].type !== "item") continue; if (itemIndex === selectedMenuIndex) { - var menuItem = menuItems[i]; + const menuItem = menuItems[i]; if (menuItem.action) menuItem.action(); else if (menuItem.pluginAction) @@ -325,209 +411,233 @@ Popup { } } - width: menuContainer.implicitWidth - height: menuContainer.implicitHeight - padding: 0 - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - modal: true - dim: false - background: Item {} + PanelWindow { + id: menuWindow - onOpened: { - Qt.callLater(() => keyboardHandler.forceActiveFocus()); - } + screen: root.targetScreen + visible: root.renderActive + color: "transparent" - onClosed: { - if (parentHandler) - parentHandler.enabled = true; - if (searchField?.visible) { - Qt.callLater(() => searchField.forceActiveFocus()); - } - } + WlrLayershell.namespace: "dms:launcher-context-menu" + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: root.renderActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None - enter: Transition { - NumberAnimation { - property: "opacity" - from: 0 - to: 1 - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } - - exit: Transition { - NumberAnimation { - property: "opacity" - from: 1 - to: 0 - duration: Theme.shortDuration - easing.type: Theme.emphasizedEasing - } - } - - contentItem: Item { - id: keyboardHandler - focus: true - implicitWidth: menuContainer.implicitWidth - implicitHeight: menuContainer.implicitHeight - - Keys.onPressed: event => { - switch (event.key) { - case Qt.Key_Down: - root.selectNext(); - event.accepted = true; - return; - case Qt.Key_Up: - root.selectPrevious(); - event.accepted = true; - return; - case Qt.Key_Return: - case Qt.Key_Enter: - root.activateSelected(); - event.accepted = true; - return; - case Qt.Key_Escape: - case Qt.Key_Left: - root.hide(); - event.accepted = true; - return; - } + anchors { + top: true + left: true + right: true + bottom: true } - Rectangle { - id: menuContainer + WindowBlur { + targetWindow: menuWindow + blurX: root.blurActive ? menuContainer.x : 0 + blurY: root.blurActive ? menuContainer.y : 0 + blurWidth: root.blurActive ? menuContainer.width : 0 + blurHeight: root.blurActive ? menuContainer.height : 0 + blurRadius: Theme.cornerRadius + } + + MouseArea { anchors.fill: parent - implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2) - implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2 - color: Theme.floatingSurface - radius: Theme.cornerRadius - border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: BlurService.enabled ? BlurService.borderWidth : 1 + z: -1 + enabled: root.renderActive + onClicked: root.hide() + } + + Item { + id: keyboardHandler + anchors.fill: parent + focus: root.openState + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Down: + root.selectNext(); + event.accepted = true; + return; + case Qt.Key_Up: + root.selectPrevious(); + event.accepted = true; + return; + case Qt.Key_Return: + case Qt.Key_Enter: + root.activateSelected(); + event.accepted = true; + return; + case Qt.Key_Escape: + case Qt.Key_Left: + root.hide(); + event.accepted = true; + return; + } + } Rectangle { - anchors.fill: parent - anchors.topMargin: 4 - anchors.leftMargin: 2 - anchors.rightMargin: -2 - anchors.bottomMargin: -4 - radius: parent.radius - color: Qt.rgba(0, 0, 0, 0.15) - z: -1 - } + id: menuContainer + x: Math.max(root.menuMargin, Math.min(menuWindow.width - width - root.menuMargin, root.anchorX)) + y: Math.max(root.menuMargin, Math.min(menuWindow.height - height - root.menuMargin, root.anchorY)) + width: root.effectiveMenuWidth + height: root.effectiveMenuHeight + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: BlurService.enabled ? BlurService.borderWidth : 1 + opacity: root.openState ? 1 : 0 - Column { - id: menuColumn - anchors.fill: parent - anchors.margins: Theme.spacingS - spacing: 1 - - Repeater { - model: root.menuItems - - Item { - id: menuItemDelegate - required property var modelData - required property int index - - width: menuColumn.width - height: modelData.type === "separator" ? 5 : 32 - - readonly property int itemIndex: { - var count = 0; - for (var i = 0; i < index; i++) { - if (root.menuItems[i].type === "item") - count++; - } - return count; - } - - Rectangle { - visible: menuItemDelegate.modelData.type === "separator" - width: parent.width - Theme.spacingS * 2 - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - color: "transparent" - - Rectangle { - anchors.centerIn: parent - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + onRunningChanged: { + if (!running && !root.openState) { + root.renderActive = false; + if (root.parentHandler) + root.parentHandler.enabled = true; + if (root.searchField?.visible) + Qt.callLater(() => root.searchField.forceActiveFocus()); } } + } + } - Rectangle { - visible: menuItemDelegate.modelData.type === "item" - width: parent.width - height: parent.height - radius: Theme.cornerRadius - color: { - if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2); + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: -1 + } + + Flickable { + id: menuFlickable + anchors.fill: parent + anchors.margins: Theme.spacingS + clip: true + contentWidth: width + contentHeight: menuColumn.implicitHeight + boundsBehavior: Flickable.StopAtBounds + interactive: root.menuScrolls + + Column { + id: menuColumn + width: menuFlickable.width + spacing: 1 + + Repeater { + id: menuRepeater + model: root.menuItems + + Item { + id: menuItemDelegate + required property var modelData + required property int index + + width: menuColumn.width + height: modelData.type === "separator" ? 5 : 32 + + readonly property int itemIndex: { + let count = 0; + for (let i = 0; i < index; i++) { + if (root.menuItems[i].type === "item") + count++; + } + return count; } - return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; - } - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingS - anchors.right: parent.right - anchors.rightMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS + Rectangle { + visible: menuItemDelegate.modelData.type === "separator" + width: parent.width - Theme.spacingS * 2 + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + color: "transparent" - Item { - width: Theme.iconSize - 2 - height: Theme.iconSize - 2 - anchors.verticalCenter: parent.verticalCenter - - DankIcon { - visible: (menuItemDelegate.modelData?.icon ?? "").length > 0 - name: menuItemDelegate.modelData?.icon ?? "" - size: Theme.iconSize - 2 - color: Theme.surfaceText - opacity: 0.7 - anchors.verticalCenter: parent.verticalCenter + Rectangle { + anchors.centerIn: parent + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) } } - StyledText { - text: menuItemDelegate.modelData.text || "" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Normal - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - width: parent.width - (Theme.iconSize - 2) - Theme.spacingS - } - } + Rectangle { + visible: menuItemDelegate.modelData.type === "item" + width: parent.width + height: parent.height + radius: Theme.cornerRadius + color: { + if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2); + } + return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; + } - DankRipple { - id: menuItemRipple - rippleColor: Theme.surfaceText - cornerRadius: Theme.cornerRadius - } + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS - MouseArea { - id: itemMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: { - root.keyboardNavigation = false; - root.selectedMenuIndex = menuItemDelegate.itemIndex; - } - onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y) - onClicked: { - var menuItem = menuItemDelegate.modelData; - if (menuItem.action) - menuItem.action(); - else if (menuItem.pluginAction) - root.executePluginAction(menuItem.pluginAction); - else if (menuItem.launcherActionData) - root.executeLauncherAction(menuItem.launcherActionData); - else if (menuItem.actionData) - root.executeDesktopAction(menuItem.actionData); + Item { + width: Theme.iconSize - 2 + height: Theme.iconSize - 2 + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + visible: (menuItemDelegate.modelData?.icon ?? "").length > 0 + name: menuItemDelegate.modelData?.icon ?? "" + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: menuItemDelegate.modelData.text || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: parent.width - (Theme.iconSize - 2) - Theme.spacingS + } + } + + DankRipple { + id: menuItemRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: itemMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: { + root.keyboardNavigation = false; + root.selectedMenuIndex = menuItemDelegate.itemIndex; + } + onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y) + onClicked: { + const menuItem = menuItemDelegate.modelData; + if (menuItem.action) + menuItem.action(); + else if (menuItem.pluginAction) + root.executePluginAction(menuItem.pluginAction); + else if (menuItem.launcherActionData) + root.executeLauncherAction(menuItem.launcherActionData); + else if (menuItem.actionData) + root.executeDesktopAction(menuItem.actionData); + } + } } } } diff --git a/quickshell/Modals/DankLauncherV2/SpotlightLauncherContent.qml b/quickshell/Modals/DankLauncherV2/SpotlightLauncherContent.qml index cc8d7905..6740f2e6 100644 --- a/quickshell/Modals/DankLauncherV2/SpotlightLauncherContent.qml +++ b/quickshell/Modals/DankLauncherV2/SpotlightLauncherContent.qml @@ -24,13 +24,34 @@ FocusScope { readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _maxResultsH) : 0 readonly property int _fastDuration: 90 readonly property int _resizeDuration: 110 + readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers + readonly property real _searchSurfaceAlpha: { + if (Theme.transparentBlurLayers) + return _hasQuery ? 0.34 : 0.28; + if (Theme.blurForegroundLayers) + return Math.max(Theme.popupTransparency, _hasQuery ? 0.68 : 0.74); + return _hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9); + } + readonly property color _searchSurfaceColor: Theme.withAlpha(_hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, _searchSurfaceAlpha) + readonly property color _searchWellColor: { + if (searchInput.activeFocus) + return Theme.withAlpha(Theme.primaryContainer, Theme.transparentBlurLayers ? 0.42 : 1.0); + if (Theme.transparentBlurLayers) + return Theme.ccPillInactiveBg; + return Theme.surfaceContainer; + } - implicitHeight: _searchAreaH + (_resultsH > 0 ? 1 + _resultsH : 0) + implicitHeight: _searchAreaH + _resultsH function resetScroll() { resultsList.resetScroll(); } + function closeTransientUi() { + contextMenu.hide(); + root.enabled = true; + } + function _buildRows() { const flat = searchController.flatModel || []; const sections = searchController.sections || []; @@ -121,13 +142,11 @@ FocusScope { } break; case Qt.Key_Tab: - if (_hasQuery) - _cycleCategory(false); + _cycleCategory(false); event.accepted = true; return; case Qt.Key_Backtab: - if (_hasQuery) - _cycleCategory(true); + _cycleCategory(true); event.accepted = true; return; case Qt.Key_Return: @@ -192,6 +211,7 @@ FocusScope { id: searchController active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true viewModeContext: "spotlight" + forceLinearNavigation: true onItemExecuted: { root.parentModal?.hide(); @@ -209,6 +229,21 @@ FocusScope { allowEditActions: false } + Connections { + target: root.parentModal + ignoreUnknownSignals: true + + function onSpotlightOpenChanged() { + if (!root.parentModal?.spotlightOpen) + root.closeTransientUi(); + } + + function onContentVisibleChanged() { + if (!root.parentModal?.contentVisible) + root.closeTransientUi(); + } + } + Connections { target: searchController function onModeChanged(mode) { @@ -233,7 +268,7 @@ FocusScope { id: searchBarSurface anchors.fill: parent radius: Theme.cornerRadius - color: Theme.withAlpha(root._hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, root._hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9)) + color: root._searchSurfaceColor Behavior on color { ColorAnimation { @@ -250,7 +285,7 @@ FocusScope { anchors.left: parent.left anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter - color: searchInput.activeFocus ? Theme.primaryContainer : Theme.surfaceContainer + color: root._searchWellColor DankIcon { anchors.centerIn: parent @@ -269,8 +304,8 @@ FocusScope { Row { id: categoryRow + visible: SettingsData.spotlightBarShowModeChips || root._hasQuery spacing: Theme.spacingXS - visible: root._hasQuery anchors.verticalCenter: parent.verticalCenter Repeater { @@ -376,26 +411,9 @@ FocusScope { } } - Rectangle { - anchors.top: searchBarItem.bottom - anchors.left: parent.left - anchors.right: parent.right - height: 1 - color: Theme.outlineMedium - opacity: root._resultsH > 0 ? 0.55 : 0 - - Behavior on opacity { - NumberAnimation { - duration: root._fastDuration - easing.type: Theme.standardEasing - } - } - } - Item { id: resultsContainer anchors.top: searchBarItem.bottom - anchors.topMargin: 1 anchors.left: parent.left anchors.right: parent.right clip: true diff --git a/quickshell/Modals/DankLauncherV2/SpotlightResultsList.qml b/quickshell/Modals/DankLauncherV2/SpotlightResultsList.qml index 1dcf8e71..93da2a44 100644 --- a/quickshell/Modals/DankLauncherV2/SpotlightResultsList.qml +++ b/quickshell/Modals/DankLauncherV2/SpotlightResultsList.qml @@ -12,6 +12,7 @@ Item { property var controller: null property bool hasQuery: false property var rows: [] + readonly property real bottomInset: Theme.spacingS signal itemRightClicked(int index, var item, real mouseX, real mouseY) @@ -53,7 +54,11 @@ Item { DankListView { id: mainListView - anchors.fill: parent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: root.bottomInset clip: true visible: root.rows.length > 0 @@ -64,11 +69,6 @@ Item { objectProp: "_rowId" } - add: null - remove: null - displaced: null - move: null - delegate: Item { id: delegateRoot required property var modelData @@ -103,7 +103,11 @@ Item { } Item { - anchors.fill: parent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: root.bottomInset visible: root.hasQuery && root.rows.length === 0 Row { diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index a2a04ee1..35301001 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -64,6 +64,7 @@ FocusScope { sourceComponent: KeybindsTab { parentModal: root.parentModal + requestedSearchQuery: root.parentModal?.keybindSearchQuery ?? "" } onActiveChanged: { diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index 366c8669..99fc2f19 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -37,6 +37,7 @@ FloatingWindow { property bool isCompactMode: width < 700 property bool menuVisible: !isCompactMode property bool enableAnimations: true + property string keybindSearchQuery: "" signal closingModal @@ -73,6 +74,11 @@ FloatingWindow { return sidebar.resolveTabIndex(tabName); } + function showKeybindsSearch(query: string) { + keybindSearchQuery = query || ""; + showWithTabName("keybinds"); + } + function toggleMenu() { enableAnimations = true; menuVisible = !menuVisible; diff --git a/quickshell/Modules/Settings/KeybindsTab.qml b/quickshell/Modules/Settings/KeybindsTab.qml index eb0239b8..af3eff7a 100644 --- a/quickshell/Modules/Settings/KeybindsTab.qml +++ b/quickshell/Modules/Settings/KeybindsTab.qml @@ -16,6 +16,7 @@ Item { property var parentModal: null property string selectedCategory: "" property string searchQuery: "" + property string requestedSearchQuery: "" property string expandedKey: "" property bool showingNewBind: false @@ -206,13 +207,34 @@ Item { } } - Component.onCompleted: _ensureCurrentProvider() + function _applyRequestedSearch() { + if (!requestedSearchQuery) + return; + const query = requestedSearchQuery; + selectedCategory = ""; + searchField.text = query; + searchQuery = query; + _updateFiltered(); + if (parentModal?.keybindSearchQuery === query) + parentModal.keybindSearchQuery = ""; + Qt.callLater(scrollToTop); + } + + Component.onCompleted: { + _ensureCurrentProvider(); + Qt.callLater(_applyRequestedSearch); + } + + onRequestedSearchQueryChanged: Qt.callLater(_applyRequestedSearch) onVisibleChanged: { if (!visible) return; - Qt.callLater(scrollToTop); _ensureCurrentProvider(); + Qt.callLater(() => { + _applyRequestedSearch(); + scrollToTop(); + }); } DankFlickable { diff --git a/quickshell/Modules/Settings/LauncherTab.qml b/quickshell/Modules/Settings/LauncherTab.qml index 0ccfcef1..a00dc4b8 100644 --- a/quickshell/Modules/Settings/LauncherTab.qml +++ b/quickshell/Modules/Settings/LauncherTab.qml @@ -9,6 +9,37 @@ Item { id: root property var parentModal: null + readonly property string defaultLauncherAction: "spawn dms ipc call spotlight toggle" + readonly property string spotlightBarAction: "spawn dms ipc call spotlight-bar toggle" + readonly property int keybindDataVersion: KeybindsService._dataVersion + readonly property bool keybindsAvailable: KeybindsService.available + readonly property string defaultLauncherKeybindSearch: "spotlight toggle" + readonly property string spotlightBarKeybindSearch: "spotlight-bar" + + function openKeybindsSearch(query) { + if (!root.parentModal) + return; + if (typeof root.parentModal.showKeybindsSearch === "function") { + root.parentModal.showKeybindsSearch(query); + } else { + root.parentModal.showWithTabName("keybinds"); + } + } + + function keysLabel(actionId) { + void (keybindDataVersion); + if (!keybindsAvailable) + return I18n.tr("Manual config"); + const keys = KeybindsService.keysForAction(actionId); + if (!keys || keys.length === 0) + return I18n.tr("Not bound"); + return keys.join(", "); + } + + Component.onCompleted: { + if (KeybindsService.available) + KeybindsService.loadBinds(false); + } FileBrowserModal { id: logoFileBrowser @@ -35,20 +66,20 @@ Item { SettingsCard { width: parent.width iconName: "search" - title: I18n.tr("Launcher Style") + title: I18n.tr("Default Launcher") settingKey: "launcherStyle" SettingsControlledByFrame { visible: SettingsData.connectedFrameModeActive parentModal: root.parentModal - settingLabel: I18n.tr("Launcher Style") - reason: I18n.tr("Managed by Frame Mode") + settingLabel: I18n.tr("Default Launcher") + reason: I18n.tr("Connected Frame Mode uses the connected launcher for default launcher shortcuts.") } StyledText { width: parent.width visible: !SettingsData.connectedFrameModeActive - text: SettingsData.launcherStyle === "spotlight" ? I18n.tr("Minimal Spotlight-style bar: appears instantly at the top of the screen and expands as you type.") : I18n.tr("Full-featured launcher with mode tabs, grid view, and action panel.") + text: SettingsData.launcherStyle === "spotlight" ? I18n.tr("Default launcher shortcuts open the minimal Spotlight Bar. The dedicated Spotlight Bar shortcut below stays independent.") : I18n.tr("Default launcher shortcuts open the full launcher with mode tabs, grid view, and action panel.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap @@ -57,8 +88,8 @@ Item { SettingsButtonGroupRow { visible: !SettingsData.connectedFrameModeActive settingKey: "launcherStyleSelector" - tags: ["launcher", "style", "spotlight", "full", "minimal"] - text: I18n.tr("Style") + tags: ["launcher", "style", "default", "spotlight", "full", "minimal"] + text: I18n.tr("Default Opens") model: [I18n.tr("Full"), I18n.tr("Spotlight")] currentIndex: SettingsData.launcherStyle === "spotlight" ? 1 : 0 onSelectionChanged: (index, selected) => { @@ -68,6 +99,76 @@ Item { } } + StyledRect { + id: defaultShortcutCard + width: parent.width + height: defaultShortcutRow.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: defaultShortcutMouse.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.48) : Theme.withAlpha(Theme.surfaceContainer, 0.35) + border.color: Theme.outlineMedium + border.width: 1 + + Row { + id: defaultShortcutRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: "keyboard" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: Math.max(0, parent.width - Theme.iconSize - defaultShortcutValue.width - Theme.spacingM * 2) + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: I18n.tr("Default Launcher Shortcut") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + } + + StyledText { + text: !root.keybindsAvailable ? I18n.tr("Bind the spotlight IPC action in your compositor config.") : SettingsData.connectedFrameModeActive ? I18n.tr("Opens the connected launcher in Connected Frame Mode.") : I18n.tr("Follows the default launcher choice selected above.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + } + } + + StyledText { + id: defaultShortcutValue + text: root.keysLabel(root.defaultLauncherAction) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignRight + width: Math.min(170, implicitWidth) + elide: Text.ElideRight + } + } + + MouseArea { + id: defaultShortcutMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.openKeybindsSearch(root.defaultLauncherKeybindSearch) + } + } + SettingsToggleRow { settingKey: "launcherUseOverlayLayer" tags: ["launcher", "fullscreen", "overlay", "layer"] @@ -78,6 +179,100 @@ Item { } } + SettingsCard { + width: parent.width + iconName: "search" + title: I18n.tr("Spotlight Bar") + settingKey: "spotlightBarLauncher" + + StyledText { + width: parent.width + text: I18n.tr("A separate minimal launcher action that works in Standalone, Separate Frame Mode, and Connected Frame Mode.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + StyledRect { + id: spotlightShortcutCard + width: parent.width + height: spotlightShortcutRow.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: spotlightShortcutMouse.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.48) : Theme.withAlpha(Theme.surfaceContainer, 0.35) + border.color: Theme.outlineMedium + border.width: 1 + + Row { + id: spotlightShortcutRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: "keyboard" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: Math.max(0, parent.width - Theme.iconSize - spotlightShortcutValue.width - Theme.spacingM * 2) + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: I18n.tr("Spotlight Bar Shortcut") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + } + + StyledText { + text: !root.keybindsAvailable ? I18n.tr("Bind the spotlight-bar IPC action in your compositor config.") : I18n.tr("Uses the spotlight-bar IPC action and always opens the minimal bar.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + } + } + + StyledText { + id: spotlightShortcutValue + text: root.keysLabel(root.spotlightBarAction) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignRight + width: Math.min(170, implicitWidth) + elide: Text.ElideRight + } + } + + MouseArea { + id: spotlightShortcutMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.openKeybindsSearch(root.spotlightBarKeybindSearch) + } + } + + SettingsToggleRow { + settingKey: "spotlightBarShowModeChips" + tags: ["launcher", "spotlight", "bar", "chips", "tabs", "modes"] + text: I18n.tr("Show Mode Chips") + description: I18n.tr("Show All, Apps, Files, and Plugins chips beside the Spotlight Bar input.") + checked: SettingsData.spotlightBarShowModeChips + onToggled: checked => SettingsData.set("spotlightBarShowModeChips", checked) + } + } + SettingsCard { width: parent.width iconName: "apps" diff --git a/quickshell/Services/AppSearchService.qml b/quickshell/Services/AppSearchService.qml index 062c1f8e..4ac2a37e 100644 --- a/quickshell/Services/AppSearchService.qml +++ b/quickshell/Services/AppSearchService.qml @@ -9,6 +9,7 @@ import qs.Services Singleton { id: root readonly property var log: Log.scoped("AppSearchService") + property int refCount: 0 property var applications: [] property var _cachedCategories: null @@ -297,11 +298,11 @@ Singleton { function getBuiltInLauncherItems(pluginId, query) { if (pluginId === "dms_clipboard_search") { const trimmed = (query || "").toString().trim(); - const entries = ClipboardService.getCachedLauncherSearchEntries(trimmed, 20); + const entries = ClipboardService.internalEntries.length > 0 ? ClipboardService.getLauncherEntries(trimmed, 20, 0) : ClipboardService.getCachedLauncherSearchEntries(trimmed, 20); return entries.map(entry => ({ - type: "clipboard", - data: entry - })); + type: "clipboard", + data: entry + })); } if (pluginId !== "dms_settings_search") diff --git a/quickshell/Services/KeybindsService.qml b/quickshell/Services/KeybindsService.qml index b9940bb6..7010fbca 100644 --- a/quickshell/Services/KeybindsService.qml +++ b/quickshell/Services/KeybindsService.qml @@ -463,6 +463,24 @@ Singleton { return _flatCache; } + function keysForAction(actionId) { + if (!actionId) + return []; + for (let i = 0; i < _flatCache.length; i++) { + const group = _flatCache[i]; + if (!group || group.action !== actionId || !Array.isArray(group.keys)) + continue; + const keys = []; + for (let k = 0; k < group.keys.length; k++) { + const key = group.keys[k]?.key || ""; + if (key) + keys.push(key); + } + return keys; + } + return []; + } + function saveBind(originalKey, bindData) { if (!bindData.key || !Actions.isValidAction(bindData.action)) return; diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index 07d111fe..a0d364ca 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -205,6 +205,8 @@ Singleton { } function launchDesktopEntry(desktopEntry, useNvidia) { + if (!desktopEntry || !desktopEntry.command) + return; let cmd = desktopEntry.command; const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || ""; @@ -261,6 +263,8 @@ Singleton { } function launchDesktopAction(desktopEntry, action, useNvidia) { + if (!desktopEntry || !action || !action.command) + return; let cmd = action.command; const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || ""; diff --git a/quickshell/Widgets/DankTextField.qml b/quickshell/Widgets/DankTextField.qml index e484170e..d56983bb 100644 --- a/quickshell/Widgets/DankTextField.qml +++ b/quickshell/Widgets/DankTextField.qml @@ -40,8 +40,8 @@ StyledRect { property color focusedBorderColor: Theme.primary property color normalBorderColor: Theme.outlineMedium property color placeholderColor: Theme.outlineButton - property int borderWidth: 1 - property int focusedBorderWidth: 2 + property real borderWidth: 1 + property real focusedBorderWidth: 2 property real cornerRadius: Theme.cornerRadius readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0) readonly property real rightPadding: { diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 153ca42c..12da5d9f 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -939,7 +939,7 @@ "topbar", "wayland" ], - "description": "Place this bar on the Wayland overlay layer" + "description": "Place the bar on the Wayland overlay layer" }, { "section": "barVisibility", @@ -967,7 +967,7 @@ "wayland" ], "icon": "visibility_off", - "description": "Place this bar on the Wayland overlay layer" + "description": "Place the bar on the Wayland overlay layer" }, { "section": "workspaceDragReorder", @@ -2052,6 +2052,50 @@ ], "icon": "extension" }, + { + "section": "launcherStyle", + "label": "Default Launcher", + "tabIndex": 9, + "category": "Launcher", + "keywords": [ + "app drawer", + "app menu", + "applications", + "default", + "drawer", + "full", + "launcher", + "layer", + "menu", + "minimal", + "opening", + "overlay", + "spotlight", + "start", + "start menu", + "style" + ], + "icon": "search", + "description": "Use the overlay layer when opening the launcher" + }, + { + "section": "launcherStyleSelector", + "label": "Default Opens", + "tabIndex": 9, + "category": "Launcher", + "keywords": [ + "default", + "drawer", + "full", + "launcher", + "menu", + "minimal", + "opens", + "spotlight", + "start", + "style" + ] + }, { "section": "niriOverviewOverlayEnabled", "label": "Enable Overview Overlay", @@ -2228,31 +2272,6 @@ ], "icon": "apps" }, - { - "section": "launcherStyle", - "label": "Launcher Style", - "tabIndex": 9, - "category": "Launcher", - "keywords": [ - "app drawer", - "app menu", - "applications", - "drawer", - "full", - "launcher", - "layer", - "menu", - "minimal", - "opening", - "overlay", - "spotlight", - "start", - "start menu", - "style" - ], - "icon": "search", - "description": "Use the overlay layer when opening the launcher" - }, { "section": "spotlightCloseNiriOverview", "label": "Niri Integration", @@ -2394,6 +2413,40 @@ ], "description": "Show mode tabs and keyboard hints at the bottom." }, + { + "section": "spotlightBarShowModeChips", + "label": "Show Mode Chips", + "tabIndex": 9, + "category": "Launcher", + "keywords": [ + "addons", + "apps", + "bar", + "beside", + "chips", + "day", + "drawer", + "extensions", + "files", + "input", + "launcher", + "light mode", + "menu", + "mode", + "modes", + "panel", + "plugins", + "show", + "spotlight", + "start", + "statusbar", + "tabs", + "taskbar", + "topbar", + "widgets" + ], + "description": "Show All, Apps, Files, and Plugins chips beside the Spotlight Bar input." + }, { "section": "launcherLogoSizeOffset", "label": "Size Offset", @@ -2458,20 +2511,38 @@ "description": "When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency." }, { - "section": "launcherStyleSelector", - "label": "Style", + "section": "spotlightBarLauncher", + "label": "Spotlight Bar", "tabIndex": 9, "category": "Launcher", "keywords": [ + "addons", + "apps", + "bar", + "beside", + "chips", + "day", "drawer", - "full", + "extensions", + "files", + "input", "launcher", + "light mode", "menu", - "minimal", + "modes", + "panel", + "plugins", + "show", "spotlight", "start", - "style" - ] + "statusbar", + "tabs", + "taskbar", + "topbar", + "widgets" + ], + "icon": "search", + "description": "Show All, Apps, Files, and Plugins chips beside the Spotlight Bar input." }, { "section": "dankLauncherV2BorderThickness",