pragma ComponentBehavior: Bound import QtQuick import qs.Common import qs.Services import qs.Widgets FocusScope { id: root LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true property var parentModal: null property string viewModeContext: "spotlight" property alias searchField: searchField property alias controller: controller property alias resultsList: resultsList property alias actionPanel: actionPanel readonly property alias activeContextMenu: contextMenu property var transientSurfaceTracker: null 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(); } function focusSearchField() { searchField.forceActiveFocus(); } function closeTransientUi() { transientSurfaceTracker?.closeAll?.(); actionPanel.hide(); root.enabled = true; } function openEditMode(app) { if (!app) return; editingApp = app; editAppId = app.id || app.execString || app.exec || ""; editMode = true; } function closeEditMode() { editMode = false; editingApp = null; editAppId = ""; Qt.callLater(() => searchField.forceActiveFocus()); } function showContextMenu(item, x, y, fromKeyboard) { if (!item) return; if (!contextMenu.hasContextMenuActions(item)) return; contextMenu.show(x, y, item, fromKeyboard); } anchors.fill: parent focus: true Controller { id: controller active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true viewModeContext: root.viewModeContext onItemExecuted: { if (root.parentModal) { root.parentModal.hide(); } if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) { NiriService.toggleOverview(); } } } LauncherContextMenu { id: contextMenu parent: root controller: root.controller searchField: root.searchField parentHandler: root transientSurfaceTracker: root.transientSurfaceTracker onEditAppRequested: app => { root.openEditMode(app); } } 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) { closeEditMode(); event.accepted = true; } return; } var hasCtrl = event.modifiers & Qt.ControlModifier; var hasAlt = event.modifiers & Qt.AltModifier; event.accepted = true; switch (event.key) { case Qt.Key_Escape: if (actionPanel.expanded) { actionPanel.hide(); return; } if (controller.clearPluginFilter()) return; if (root.parentModal) root.parentModal.hide(); return; case Qt.Key_Backspace: if (searchField.text.length === 0) { if (controller.clearPluginFilter()) return; if (controller.autoSwitchedToFiles) { controller.restorePreviousMode(); return; } } event.accepted = false; return; case Qt.Key_Down: controller.selectNext(); return; case Qt.Key_Up: controller.selectPrevious(); return; case Qt.Key_PageDown: controller.selectPageDown(8); return; case Qt.Key_PageUp: controller.selectPageUp(8); return; case Qt.Key_Right: if (controller.getCurrentSectionViewMode() !== "list") { controller.selectRight(); return; } event.accepted = false; return; case Qt.Key_Left: if (controller.getCurrentSectionViewMode() !== "list") { controller.selectLeft(); return; } event.accepted = false; return; case Qt.Key_J: if (hasCtrl) { controller.selectNext(); return; } event.accepted = false; return; case Qt.Key_K: if (hasCtrl) { controller.selectPrevious(); return; } event.accepted = false; return; case Qt.Key_L: if (hasCtrl) { if (controller.getCurrentSectionViewMode() !== "list") { controller.selectRight(); } return; } event.accepted = false; return; case Qt.Key_H: if (hasCtrl) { if (controller.getCurrentSectionViewMode() !== "list") { controller.selectLeft(); } return; } event.accepted = false; return; case Qt.Key_N: if (hasCtrl) { controller.selectNextSection(); return; } event.accepted = false; return; case Qt.Key_P: if (hasCtrl) { controller.selectPreviousSection(); return; } event.accepted = false; return; case Qt.Key_Tab: if (actionPanel.hasActions) { actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show(); } return; case Qt.Key_Backtab: if (actionPanel.expanded) actionPanel.hide(); return; case Qt.Key_Return: case Qt.Key_Enter: if (event.modifiers & Qt.ShiftModifier) { controller.pasteSelected(); return; } if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) { actionPanel.executeSelectedAction(); } else { controller.executeSelected(); } return; case Qt.Key_Menu: case Qt.Key_F10: if (contextMenu.hasContextMenuActions(controller.selectedItem)) { var scenePos = resultsList.getSelectedItemPosition(); var localPos = root.mapFromItem(null, scenePos.x, scenePos.y); showContextMenu(controller.selectedItem, localPos.x, localPos.y, true); } return; case Qt.Key_1: if (hasCtrl || hasAlt) { controller.setMode("all"); return; } event.accepted = false; return; case Qt.Key_2: if (hasCtrl || hasAlt) { controller.setMode("apps"); return; } event.accepted = false; return; case Qt.Key_3: if (hasCtrl || hasAlt) { controller.setMode("files"); return; } event.accepted = false; return; case Qt.Key_4: if (hasCtrl || hasAlt) { controller.setMode("plugins"); return; } event.accepted = false; return; default: event.accepted = false; } } Item { id: contentHolder anchors.fill: parent visible: !editMode readonly property bool inverted: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "top") readonly property bool _connectedArcAtHeader: inverted && !(root.parentModal?.launcherArcExtenderActive ?? false) Item { id: footerBar readonly property bool _connectedBottomEmerge: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "bottom") readonly property bool _connectedArcAtFooter: _connectedBottomEmerge && !(root.parentModal?.launcherArcExtenderActive ?? false) readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: root.parentModal?.borderWidth ?? 1 anchors.rightMargin: root.parentModal?.borderWidth ?? 1 y: contentHolder.inverted ? 0 : (parent.height - height - (_connectedBottomEmerge ? 0 : (root.parentModal?.borderWidth ?? 1))) height: showFooter ? ((_connectedArcAtFooter || contentHolder._connectedArcAtHeader) ? 76 : 36) : 0 visible: showFooter clip: true Rectangle { 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) && !root._blurActive color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) radius: Theme.cornerRadius } Row { id: modeButtonsRow anchors.left: parent.left anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight spacing: Theme.spacingXXS Repeater { model: [ { id: "all", label: I18n.tr("All"), icon: "search" }, { id: "apps", label: I18n.tr("Apps"), icon: "apps" }, { id: "files", label: I18n.tr("Files"), icon: "folder" }, { id: "plugins", label: I18n.tr("Plugins"), icon: "extension" } ] Rectangle { required property var modelData required property int index width: buttonContent.width + Theme.spacingM * 2 height: 28 radius: Theme.cornerRadius color: controller.searchMode === modelData.id ? Theme.buttonBg : modeArea.containsMouse ? Theme.surfaceContainerHighest : Theme.withAlpha(Theme.surfaceContainerHighest, 0) Row { id: buttonContent anchors.centerIn: parent spacing: Theme.spacingXS DankIcon { anchors.verticalCenter: parent.verticalCenter name: modelData.icon size: 14 color: controller.searchMode === modelData.id ? Theme.buttonText : Theme.surfaceVariantText } StyledText { anchors.verticalCenter: parent.verticalCenter text: modelData.label font.pixelSize: Theme.fontSizeSmall color: controller.searchMode === modelData.id ? Theme.buttonText : Theme.surfaceText } } MouseArea { id: modeArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: controller.setMode(modelData.id) } } } } Row { id: hintsRow anchors.right: parent.right anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight spacing: Theme.spacingM StyledText { anchors.verticalCenter: parent.verticalCenter text: "↑↓ " + I18n.tr("nav") font.pixelSize: Theme.fontSizeSmall - 1 color: Theme.surfaceVariantText } StyledText { anchors.verticalCenter: parent.verticalCenter text: "↵ " + I18n.tr("Open") font.pixelSize: Theme.fontSizeSmall - 1 color: Theme.surfaceVariantText } StyledText { anchors.verticalCenter: parent.verticalCenter text: "Tab " + I18n.tr("Actions") font.pixelSize: Theme.fontSizeSmall - 1 color: Theme.surfaceVariantText visible: actionPanel.hasActions } } } Row { id: searchRow spacing: Theme.spacingS anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM y: contentHolder.inverted ? (parent.height - height - Theme.spacingM) : Theme.spacingM Rectangle { id: pluginBadge visible: controller.activePluginName.length > 0 width: visible ? pluginBadgeContent.implicitWidth + Theme.spacingM : 0 height: searchField.height radius: 16 color: Theme.primary Row { id: pluginBadgeContent anchors.centerIn: parent spacing: Theme.spacingXS DankIcon { anchors.verticalCenter: parent.verticalCenter name: "extension" size: 14 color: Theme.primaryText } StyledText { anchors.verticalCenter: parent.verticalCenter text: controller.activePluginName font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: Theme.primaryText } } Behavior on width { NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } } DankTextField { id: searchField width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0) cornerRadius: Theme.cornerRadius 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 leftIconFocusedColor: Theme.primary showClearButton: true textColor: Theme.surfaceText font.pixelSize: Theme.fontSizeLarge enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true placeholderText: "" ignoreUpDownKeys: true ignoreTabKeys: true keyForwardTargets: [root] onTextChanged: { controller.setSearchQuery(text); if (actionPanel.expanded) { actionPanel.hide(); } } Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { if (root.parentModal) { root.parentModal.hide(); } event.accepted = true; } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter)) { if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) { actionPanel.executeSelectedAction(); } else { controller.executeSelected(); } event.accepted = true; } } } } Item { id: contentStack anchors.left: parent.left anchors.right: parent.right anchors.top: contentHolder.inverted ? footerBar.bottom : searchRow.bottom anchors.bottom: contentHolder.inverted ? searchRow.top : footerBar.top anchors.leftMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM anchors.topMargin: contentHolder.inverted && !footerBar.showFooter ? Theme.spacingM : contentStack.gap anchors.bottomMargin: contentHolder.inverted ? contentStack.gap : 0 readonly property real gap: Theme.spacingXS clip: false Row { id: categoryRow width: parent.width readonly property bool showPluginCategories: controller.activePluginCategories.length > 0 height: showPluginCategories ? 36 : 0 visible: showPluginCategories spacing: Theme.spacingS anchors.top: parent.top anchors.topMargin: 0 clip: true Behavior on height { NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } DankDropdown { id: categoryDropdown visible: categoryRow.showPluginCategories width: Math.min(200, parent.width) compactMode: true dropdownWidth: 200 popupWidth: 240 maxPopupHeight: 300 enableFuzzySearch: controller.activePluginCategories.length > 8 currentValue: { const cats = controller.activePluginCategories; const current = controller.activePluginCategory; if (!current) return cats.length > 0 ? cats[0].name : ""; for (let i = 0; i < cats.length; i++) { if (cats[i].id === current) return cats[i].name; } return cats.length > 0 ? cats[0].name : ""; } options: { const cats = controller.activePluginCategories; const names = []; for (let i = 0; i < cats.length; i++) names.push(cats[i].name); return names; } onValueChanged: value => { const cats = controller.activePluginCategories; for (let i = 0; i < cats.length; i++) { if (cats[i].name === value) { controller.setActivePluginCategory(cats[i].id); return; } } } } } Item { id: fileFilterRow width: parent.width height: showFileFilters ? fileFilterContent.height : 0 visible: showFileFilters anchors.top: parent.top anchors.topMargin: 0 readonly property bool showFileFilters: controller.searchMode === "files" Behavior on height { NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } Row { id: fileFilterContent width: parent.width spacing: Theme.spacingS Row { id: typeChips anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingXXS visible: DSearchService.supportsTypeFilter Repeater { model: [ { id: "all", label: I18n.tr("All"), icon: "search" }, { id: "file", label: I18n.tr("Files"), icon: "insert_drive_file" }, { id: "dir", label: I18n.tr("Folders"), icon: "folder" } ] Rectangle { required property var modelData required property int index width: chipContent.width + Theme.spacingM * 2 height: sortDropdown.height radius: Theme.cornerRadius color: controller.fileSearchType === modelData.id ? Theme.buttonBg : chipArea.containsMouse ? Theme.surfaceContainerHighest : Theme.withAlpha(Theme.surfaceContainerHighest, 0) Row { id: chipContent anchors.centerIn: parent spacing: Theme.spacingXS DankIcon { anchors.verticalCenter: parent.verticalCenter name: modelData.icon size: 14 color: controller.fileSearchType === modelData.id ? Theme.buttonText : Theme.surfaceVariantText } StyledText { anchors.verticalCenter: parent.verticalCenter text: modelData.label font.pixelSize: Theme.fontSizeSmall color: controller.fileSearchType === modelData.id ? Theme.buttonText : Theme.surfaceVariantText } } MouseArea { id: chipArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: controller.setFileSearchType(modelData.id) } } } } Rectangle { width: 1 height: 20 anchors.verticalCenter: parent.verticalCenter color: Theme.outlineMedium visible: typeChips.visible } DankDropdown { id: sortDropdown anchors.verticalCenter: parent.verticalCenter width: Math.min(130, parent.width / 3) compactMode: true dropdownWidth: 130 popupWidth: 150 maxPopupHeight: 200 currentValue: { switch (controller.fileSearchSort) { case "score": return I18n.tr("Score"); case "name": return I18n.tr("Name"); case "modified": return I18n.tr("Modified"); case "size": return I18n.tr("Size"); default: return I18n.tr("Score"); } } options: [I18n.tr("Score"), I18n.tr("Name"), I18n.tr("Modified"), I18n.tr("Size")] onValueChanged: value => { var sortMap = {}; sortMap[I18n.tr("Score")] = "score"; sortMap[I18n.tr("Name")] = "name"; sortMap[I18n.tr("Modified")] = "modified"; sortMap[I18n.tr("Size")] = "size"; controller.setFileSearchSort(sortMap[value] || "score"); } } DankTextField { id: extFilterField anchors.verticalCenter: parent.verticalCenter width: Math.min(100, parent.width / 4) height: sortDropdown.height placeholderText: I18n.tr("ext") font.pixelSize: Theme.fontSizeSmall showClearButton: text.length > 0 onTextChanged: { controller.setFileSearchExt(text.trim()); } } } } Item { id: resultsSlot width: parent.width anchors.top: fileFilterRow.visible ? fileFilterRow.bottom : (categoryRow.visible ? categoryRow.bottom : parent.top) anchors.topMargin: (fileFilterRow.visible || categoryRow.visible) ? contentStack.gap : 0 anchors.bottom: actionPanel.top anchors.bottomMargin: actionPanel.height > 0 || !contentHolder.inverted ? contentStack.gap : 0 opacity: { if (!root.parentModal) return 1; if (Theme.isDirectionalEffect && root.parentModal.isClosing) return 1; return root.parentModal.isClosing ? 0 : 1; } ResultsList { id: resultsList anchors.fill: parent controller: root.controller leadingSectionHeaderAtBottom: contentHolder.inverted transientSurfaceTracker: root.transientSurfaceTracker onItemRightClicked: (index, item, sceneX, sceneY) => { if (item && contextMenu.hasContextMenuActions(item)) { var localPos = root.mapFromItem(null, sceneX, sceneY); root.showContextMenu(item, localPos.x, localPos.y, false); } } } } ActionPanel { id: actionPanel width: parent.width anchors.bottom: parent.bottom selectedItem: controller.selectedItem controller: controller } } } Connections { target: controller function onSelectedItemChanged() { if (actionPanel.expanded && !actionPanel.hasActions) { actionPanel.hide(); } } function onSearchQueryRequested(query) { searchField.text = query; } function onModeChanged() { extFilterField.text = ""; } } Loader { id: editLoader anchors.fill: parent anchors.margins: Theme.spacingM active: root.editMode visible: active focus: root.editMode sourceComponent: AppEditView { focus: true editingApp: root.editingApp editAppId: root.editAppId onCloseRequested: root.closeEditMode() } onLoaded: item.loadOverride() } }