From 5d94c0a7d44a2255b81815a52a7056a94a39842d Mon Sep 17 00:00:00 2001 From: nippy Date: Sat, 18 Apr 2026 13:49:56 +0200 Subject: update Raveos themes --- .../Modals/DankLauncherV2/ActionPanel.qml | 256 +++ .../Modals/DankLauncherV2/Controller.qml | 1894 ++++++++++++++++++++ .../Modals/DankLauncherV2/ControllerUtils.js | 128 ++ .../Modals/DankLauncherV2/DankLauncherV2Modal.qml | 465 +++++ .../quickshell/Modals/DankLauncherV2/GridItem.qml | 105 ++ .../Modals/DankLauncherV2/ItemTransformers.js | 237 +++ .../Modals/DankLauncherV2/LauncherContent.qml | 1075 +++++++++++ .../Modals/DankLauncherV2/LauncherContextMenu.qml | 510 ++++++ .../Modals/DankLauncherV2/NavigationHelpers.js | 254 +++ .../Modals/DankLauncherV2/ResultItem.qml | 196 ++ .../Modals/DankLauncherV2/ResultsList.qml | 498 +++++ .../quickshell/Modals/DankLauncherV2/Scorer.js | 265 +++ .../quickshell/Modals/DankLauncherV2/Section.qml | 114 ++ .../Modals/DankLauncherV2/SectionHeader.qml | 340 ++++ .../quickshell/Modals/DankLauncherV2/TileItem.qml | 199 ++ 15 files changed, 6536 insertions(+) create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Controller.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ControllerUtils.js create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/GridItem.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ItemTransformers.js create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContent.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/NavigationHelpers.js create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultItem.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Scorer.js create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Section.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/SectionHeader.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/TileItem.qml (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2') diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml new file mode 100644 index 0000000..31ada32 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml @@ -0,0 +1,256 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var selectedItem: null + property var controller: null + property bool expanded: false + property int selectedActionIndex: 0 + + function getPluginContextMenuActions() { + if (selectedItem?.type !== "plugin" || !selectedItem?.pluginId) + return []; + var instance = PluginService.pluginInstances[selectedItem.pluginId]; + if (!instance) + return []; + if (typeof instance.getContextMenuActions !== "function") + return []; + var actions = instance.getContextMenuActions(selectedItem.data); + if (!Array.isArray(actions)) + return []; + return actions; + } + + readonly property var actions: { + var result = []; + if (selectedItem?.primaryAction) { + result.push(selectedItem.primaryAction); + } + + switch (selectedItem?.type) { + case "plugin": + var pluginActions = getPluginContextMenuActions(); + for (var i = 0; i < pluginActions.length; i++) { + var act = pluginActions[i]; + result.push({ + name: act.text || act.name || "", + icon: act.icon || "play_arrow", + action: "plugin_action", + pluginAction: act.action + }); + } + break; + case "plugin_browse": + if (selectedItem?.actions) { + for (var i = 0; i < selectedItem.actions.length; i++) { + result.push(selectedItem.actions[i]); + } + } + break; + case "app": + if (selectedItem?.isCore) + break; + if (SessionService.nvidiaCommand) { + result.push({ + name: I18n.tr("Launch on dGPU"), + icon: "memory", + action: "launch_dgpu" + }); + } + if (selectedItem?.actions) { + for (var i = 0; i < selectedItem.actions.length; i++) { + result.push(selectedItem.actions[i]); + } + } + break; + } + return result; + } + + readonly property bool hasActions: { + switch (selectedItem?.type) { + case "app": + return !selectedItem?.isCore; + case "plugin": + return getPluginContextMenuActions().length > 0; + case "plugin_browse": + return selectedItem?.actions?.length > 0; + default: + return actions.length > 1; + } + } + + width: parent?.width ?? 200 + height: expanded && hasActions ? 52 : 0 + color: Theme.surfaceContainerHigh + radius: Theme.cornerRadius + + clip: true + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Rectangle { + anchors.top: parent.top + width: parent.width + height: 1 + color: Theme.outlineMedium + } + + Item { + anchors.fill: parent + anchors.margins: Theme.spacingS + + Flickable { + id: actionsFlickable + anchors.left: parent.left + anchors.right: tabHint.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + height: parent.height + contentWidth: actionsRow.width + contentHeight: height + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.HorizontalFlick + + Row { + id: actionsRow + height: parent.height + spacing: Theme.spacingS + + Repeater { + model: root.actions + + Rectangle { + id: actionButton + + required property var modelData + required property int index + + width: actionContent.implicitWidth + Theme.spacingM * 2 + height: actionsRow.height + radius: Theme.cornerRadius + color: index === root.selectedActionIndex ? Theme.primaryHover : actionArea.containsMouse ? Theme.surfaceHover : "transparent" + + Row { + id: actionContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: actionButton.modelData?.icon ?? "play_arrow" + size: 16 + color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: actionButton.modelData?.name ?? "" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText + } + } + + MouseArea { + id: actionArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.controller && root.selectedItem) { + root.controller.executeAction(root.selectedItem, actionButton.modelData); + } + } + onEntered: root.selectedActionIndex = actionButton.index + } + } + } + } + } + + StyledText { + id: tabHint + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + visible: root.hasActions + text: "Tab" + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.outlineButton + } + } + + function toggle() { + expanded = !expanded; + selectedActionIndex = 0; + } + + function show() { + expanded = true; + selectedActionIndex = actions.length > 1 ? 1 : 0; + } + + function hide() { + expanded = false; + selectedActionIndex = 0; + } + + function cycleAction(reverse = false) { + if (actions.length > 0) { + if (! reverse) + selectedActionIndex = (selectedActionIndex + 1) % actions.length; + else + selectedActionIndex = (selectedActionIndex - 1) % actions.length; + ensureSelectedVisible(); + } + } + + function ensureSelectedVisible() { + if (selectedActionIndex < 0 || !actionsRow.children || selectedActionIndex >= actionsRow.children.length) + return; + var buttonX = 0; + for (var i = 0; i < selectedActionIndex; i++) { + var child = actionsRow.children[i]; + if (child) + buttonX += child.width + actionsRow.spacing; + } + + var button = actionsRow.children[selectedActionIndex]; + if (!button) + return; + var buttonRight = buttonX + button.width; + var viewLeft = actionsFlickable.contentX; + var viewRight = viewLeft + actionsFlickable.width; + + if (buttonX < viewLeft) { + actionsFlickable.contentX = Math.max(0, buttonX - Theme.spacingS); + } else if (buttonRight > viewRight) { + actionsFlickable.contentX = Math.min(actionsFlickable.contentWidth - actionsFlickable.width, buttonRight - actionsFlickable.width + Theme.spacingS); + } + } + + function executeSelectedAction() { + if (!controller || !selectedItem || selectedActionIndex >= actions.length) + return; + var action = actions[selectedActionIndex]; + if (action.action === "plugin_action" && typeof action.pluginAction === "function") { + action.pluginAction(); + controller.performSearch(); + controller.itemExecuted(); + } else { + controller.executeAction(selectedItem, action); + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Controller.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Controller.qml new file mode 100644 index 0000000..67cb606 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Controller.qml @@ -0,0 +1,1894 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Services +import "Scorer.js" as Scorer +import "ControllerUtils.js" as Utils +import "NavigationHelpers.js" as Nav +import "ItemTransformers.js" as Transform + +Item { + id: root + + property string searchQuery: "" + property string searchMode: "all" + property string previousSearchMode: "all" + property bool autoSwitchedToFiles: false + property bool isFileSearching: false + property var sections: [] + property var flatModel: [] + property int selectedFlatIndex: 0 + property var selectedItem: null + property bool isSearching: false + property string activePluginId: "" + property var collapsedSections: ({}) + property bool keyboardNavigationActive: false + property bool active: false + property var _modeSectionsCache: ({}) + property bool _queryDrivenSearch: false + property bool _diskCacheConsumed: false + property var sectionViewModes: ({}) + property var pluginViewPreferences: ({}) + property int gridColumns: SettingsData.appLauncherGridColumns + property int viewModeVersion: 0 + property string viewModeContext: "spotlight" + + signal itemExecuted + signal searchCompleted + signal modeChanged(string mode) + signal queryChanged(string query) + signal viewModeChanged(string sectionId, string mode) + signal searchQueryRequested(string query) + + onActiveChanged: { + if (!active) { + SessionData.addLauncherHistory(searchQuery); + + sections = []; + flatModel = []; + selectedItem = null; + _clearModeCache(); + } + } + + onSearchModeChanged: { + if (searchMode === "apps") { + _loadAppCategories(); + } else { + appCategory = ""; + appCategories = []; + } + } + + Connections { + target: SettingsData + function onSortAppsAlphabeticallyChanged() { + AppSearchService.invalidateLauncherCache(); + _clearModeCache(); + } + } + + Connections { + target: AppSearchService + function onCacheVersionChanged() { + if (!active) + return; + _clearModeCache(); + if (searchMode === "apps") { + _loadAppCategories(); + performSearch(); + } else if (!searchQuery && searchMode === "all") { + performSearch(); + } + } + } + + Connections { + target: PluginService + function onRequestLauncherUpdate(pluginId) { + if (!active) + return; + if (activePluginId === pluginId) { + if (activePluginCategories.length <= 1) + loadPluginCategories(pluginId); + performSearch(); + return; + } + if (searchQuery) + performSearch(); + } + } + + Process { + id: wtypeProcess + command: ["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"] + running: false + } + + Process { + id: copyProcess + running: false + onExited: pasteTimer.start() + } + + Timer { + id: pasteTimer + interval: 200 + repeat: false + onTriggered: wtypeProcess.running = true + } + + function pasteSelected() { + if (!selectedItem) + return; + if (!SessionService.wtypeAvailable) { + ToastService.showError("wtype not available - install wtype for paste support"); + return; + } + + const pluginId = selectedItem.pluginId; + if (!pluginId) + return; + const pasteArgs = AppSearchService.getPluginPasteArgs(pluginId, selectedItem.data); + if (!pasteArgs) + return; + copyProcess.command = pasteArgs; + copyProcess.running = true; + itemExecuted(); + } + + readonly property var sectionDefinitions: [ + { + id: "favorites", + title: I18n.tr("Pinned"), + icon: "push_pin", + priority: 1, + defaultViewMode: "list" + }, + { + id: "apps", + title: I18n.tr("Applications"), + icon: "apps", + priority: 2, + defaultViewMode: "list" + }, + { + id: "browse_plugins", + title: I18n.tr("Browse"), + icon: "category", + priority: 2.5, + defaultViewMode: "grid" + }, + { + id: "files", + title: I18n.tr("Files"), + icon: "folder", + priority: 4, + defaultViewMode: "list" + }, + { + id: "fallback", + title: I18n.tr("Commands"), + icon: "terminal", + priority: 5, + defaultViewMode: "list" + } + ] + + property int historyIndex: -1 + property string typingBackup: "" + + function navigateHistory(direction) { + let history = SessionData.launcherQueryHistory; + if (history.length === 0) + return; + + if (historyIndex === -1) + typingBackup = searchQuery; + + let nextIndex = historyIndex + (direction === "up" ? 1 : -1); + if (nextIndex >= history.length) + nextIndex = history.length - 1; + if (nextIndex < -1) + nextIndex = -1; + + if (nextIndex === historyIndex) + return; + historyIndex = nextIndex; + + let targetText = (historyIndex === -1) ? typingBackup : history[historyIndex]; + + setSearchQuery(targetText); + searchQueryRequested(targetText); + } + + property string fileSearchType: "all" + property string fileSearchExt: "" + property string fileSearchFolder: "" + property string fileSearchSort: "score" + + property string pluginFilter: "" + property string activePluginName: "" + property var activePluginCategories: [] + property string activePluginCategory: "" + property string appCategory: "" + property var appCategories: [] + + function getSectionViewMode(sectionId) { + if (sectionId === "browse_plugins") + return "list"; + if (pluginViewPreferences[sectionId]?.enforced) + return pluginViewPreferences[sectionId].mode; + if (sectionViewModes[sectionId]) + return sectionViewModes[sectionId]; + + var savedModes = viewModeContext === "appDrawer" ? (SettingsData.appDrawerSectionViewModes || {}) : (SettingsData.spotlightSectionViewModes || {}); + if (savedModes[sectionId]) + return savedModes[sectionId]; + + for (var i = 0; i < sectionDefinitions.length; i++) { + if (sectionDefinitions[i].id === sectionId) + return sectionDefinitions[i].defaultViewMode || "list"; + } + + if (pluginViewPreferences[sectionId]?.mode) + return pluginViewPreferences[sectionId].mode; + + return "list"; + } + + function setSectionViewMode(sectionId, mode) { + if (sectionId === "browse_plugins") + return; + if (pluginViewPreferences[sectionId]?.enforced) + return; + sectionViewModes = Object.assign({}, sectionViewModes, { + [sectionId]: mode + }); + viewModeVersion++; + if (viewModeContext === "appDrawer") { + var savedModes = Object.assign({}, SettingsData.appDrawerSectionViewModes || {}, { + [sectionId]: mode + }); + SettingsData.appDrawerSectionViewModes = savedModes; + } else { + var savedModes = Object.assign({}, SettingsData.spotlightSectionViewModes || {}, { + [sectionId]: mode + }); + SettingsData.spotlightSectionViewModes = savedModes; + } + viewModeChanged(sectionId, mode); + } + + function canChangeSectionViewMode(sectionId) { + if (sectionId === "browse_plugins") + return false; + return !pluginViewPreferences[sectionId]?.enforced; + } + + function canCollapseSection(sectionId) { + return searchMode === "all"; + } + + function setPluginViewPreference(pluginId, mode, enforced) { + var prefs = Object.assign({}, pluginViewPreferences); + prefs[pluginId] = { + mode: mode, + enforced: enforced || false + }; + pluginViewPreferences = prefs; + } + + function applyActivePluginViewPreference(pluginId, isBuiltIn) { + var sectionId = "plugin_" + pluginId; + var pref = null; + if (isBuiltIn) { + var builtIn = AppSearchService.builtInPlugins[pluginId]; + if (builtIn && builtIn.viewMode) { + pref = { + mode: builtIn.viewMode, + enforced: builtIn.viewModeEnforced === true + }; + } + } else { + pref = PluginService.getPluginViewPreference(pluginId); + } + + if (pref && pref.mode) { + setPluginViewPreference(sectionId, pref.mode, pref.enforced); + } else { + var prefs = Object.assign({}, pluginViewPreferences); + delete prefs[sectionId]; + pluginViewPreferences = prefs; + } + } + + function clearActivePluginViewPreference() { + var prefs = {}; + for (var key in pluginViewPreferences) { + if (!key.startsWith("plugin_")) { + prefs[key] = pluginViewPreferences[key]; + } + } + pluginViewPreferences = prefs; + } + + property int _searchVersion: 0 + property bool _pluginPhasePending: false + property bool _pluginPhaseForceFirst: false + property var _phase1Items: [] + + Timer { + id: searchDebounce + interval: 60 + onTriggered: root.performSearch() + } + + Timer { + id: pluginPhaseTimer + interval: 1 + onTriggered: root._performPluginPhase() + } + + Timer { + id: fileSearchDebounce + interval: 200 + onTriggered: root.performFileSearch() + } + + function getOrTransformApp(app) { + return AppSearchService.getOrTransformApp(app, transformApp); + } + + function setSearchQuery(query) { + _searchVersion++; + _queryDrivenSearch = true; + _pluginPhasePending = false; + _phase1Items = []; + pluginPhaseTimer.stop(); + searchQuery = query; + searchDebounce.restart(); + + var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll); + if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) { + fileSearchDebounce.restart(); + } + } + + function setMode(mode, isAutoSwitch) { + if (searchMode === mode) + return; + if (isAutoSwitch) { + previousSearchMode = searchMode; + autoSwitchedToFiles = true; + } else { + autoSwitchedToFiles = false; + } + searchMode = mode; + modeChanged(mode); + performSearch(); + var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0; + if (mode === "files" || filesInAll) { + fileSearchDebounce.restart(); + } + } + + function restorePreviousMode() { + if (!autoSwitchedToFiles) + return; + autoSwitchedToFiles = false; + searchMode = previousSearchMode; + modeChanged(previousSearchMode); + performSearch(); + } + + function cycleMode(reverse = false) { + var modes = ["all", "apps", "files", "plugins"]; + var currentIndex = modes.indexOf(searchMode); + if (!reverse) + var nextIndex = (currentIndex + 1) % modes.length; + else + var nextIndex = (currentIndex - 1 + modes.length) % modes.length; + setMode(modes[nextIndex]); + } + + function reset() { + searchQuery = ""; + searchMode = "all"; + previousSearchMode = "all"; + autoSwitchedToFiles = false; + isFileSearching = false; + fileSearchType = "all"; + fileSearchExt = ""; + fileSearchFolder = ""; + fileSearchSort = "score"; + sections = []; + flatModel = []; + selectedFlatIndex = 0; + selectedItem = null; + isSearching = false; + activePluginId = ""; + activePluginName = ""; + activePluginCategories = []; + activePluginCategory = ""; + appCategory = ""; + appCategories = []; + pluginFilter = ""; + collapsedSections = {}; + _clearModeCache(); + _queryDrivenSearch = false; + _pluginPhasePending = false; + _pluginPhaseForceFirst = false; + _phase1Items = []; + pluginPhaseTimer.stop(); + } + + function loadPluginCategories(pluginId) { + if (!pluginId) { + if (activePluginCategories.length > 0) { + activePluginCategories = []; + activePluginCategory = ""; + } + return; + } + + const categories = AppSearchService.getPluginLauncherCategories(pluginId); + if (categories.length === activePluginCategories.length) { + let same = true; + for (let i = 0; i < categories.length; i++) { + if (categories[i].id !== activePluginCategories[i]?.id) { + same = false; + break; + } + } + if (same) + return; + } + activePluginCategories = categories; + activePluginCategory = ""; + AppSearchService.setPluginLauncherCategory(pluginId, ""); + } + + function setActivePluginCategory(categoryId) { + if (activePluginCategory === categoryId) + return; + activePluginCategory = categoryId; + AppSearchService.setPluginLauncherCategory(activePluginId, categoryId); + performSearch(); + } + + function setAppCategory(category) { + if (appCategory === category) + return; + appCategory = category; + _queryDrivenSearch = true; + _clearModeCache(); + performSearch(); + } + + function _loadAppCategories() { + appCategories = AppSearchService.getAllCategories(); + } + + function setFileSearchType(type) { + if (fileSearchType === type) + return; + fileSearchType = type; + performFileSearch(); + } + + function setFileSearchExt(ext) { + if (fileSearchExt === ext) + return; + fileSearchExt = ext; + performFileSearch(); + } + + function setFileSearchFolder(folder) { + if (fileSearchFolder === folder) + return; + fileSearchFolder = folder; + performFileSearch(); + } + + function setFileSearchSort(sort) { + if (fileSearchSort === sort) + return; + fileSearchSort = sort; + performFileSearch(); + } + + function clearPluginFilter() { + if (pluginFilter) { + pluginFilter = ""; + performSearch(); + return true; + } + return false; + } + + function preserveSelectionAfterUpdate(forceFirst) { + if (forceFirst) + return function () { + return getFirstItemIndex(); + }; + var previousSelectedId = selectedItem?.id || ""; + return function (newFlatModel) { + if (!previousSelectedId) + return getFirstItemIndex(); + for (var i = 0; i < newFlatModel.length; i++) { + if (!newFlatModel[i].isHeader && newFlatModel[i].item?.id === previousSelectedId) + return i; + } + return getFirstItemIndex(); + }; + } + + function performSearch() { + queryChanged(searchQuery); + + var currentVersion = _searchVersion; + isSearching = true; + var shouldResetSelection = _queryDrivenSearch; + _queryDrivenSearch = false; + var restoreSelection = preserveSelectionAfterUpdate(shouldResetSelection); + + var cachedSections = AppSearchService.getCachedDefaultSections(); + if (!cachedSections && !_diskCacheConsumed && !searchQuery && searchMode === "all" && !pluginFilter) { + _diskCacheConsumed = true; + var diskSections = _loadDiskCache(); + if (diskSections) { + activePluginId = ""; + activePluginName = ""; + activePluginCategories = []; + activePluginCategory = ""; + clearActivePluginViewPreference(); + for (var i = 0; i < diskSections.length; i++) { + if (collapsedSections[diskSections[i].id] !== undefined) + diskSections[i].collapsed = collapsedSections[diskSections[i].id]; + } + _applyHighlights(diskSections, ""); + flatModel = Scorer.flattenSections(diskSections); + sections = diskSections; + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + isSearching = false; + searchCompleted(); + return; + } + } + + if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) { + activePluginId = ""; + activePluginName = ""; + activePluginCategories = []; + activePluginCategory = ""; + clearActivePluginViewPreference(); + var modeCache = _getCachedModeData("all"); + if (modeCache) { + _applyHighlights(modeCache.sections, ""); + sections = modeCache.sections; + flatModel = modeCache.flatModel; + } else { + var newSections = cachedSections.map(function (s) { + var copy = Object.assign({}, s, { + items: s.items ? s.items.slice() : [] + }); + if (collapsedSections[s.id] !== undefined) + copy.collapsed = collapsedSections[s.id]; + return copy; + }); + _applyHighlights(newSections, ""); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + _setCachedModeData("all", sections, flatModel); + } + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + isSearching = false; + searchCompleted(); + return; + } + + var allItems = []; + + var triggerMatch = detectTrigger(searchQuery); + if (triggerMatch.pluginId) { + var pluginChanged = activePluginId !== triggerMatch.pluginId; + activePluginId = triggerMatch.pluginId; + activePluginName = getPluginName(triggerMatch.pluginId, triggerMatch.isBuiltIn); + applyActivePluginViewPreference(triggerMatch.pluginId, triggerMatch.isBuiltIn); + + if (pluginChanged && !triggerMatch.isBuiltIn) + loadPluginCategories(triggerMatch.pluginId); + + var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query); + for (var k = 0; k < pluginItems.length; k++) + allItems.push(pluginItems[k]); + + if (triggerMatch.isBuiltIn) { + var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query); + for (var j = 0; j < builtInItems.length; j++) { + allItems.push(transformBuiltInLauncherItem(builtInItems[j], triggerMatch.pluginId)); + } + } + + var dynamicDefs = buildDynamicSectionDefs(allItems); + var scoredItems = Scorer.scoreItems(allItems, triggerMatch.query, getFrecencyForItem); + var sortAlpha = !triggerMatch.query && SettingsData.sortAppsAlphabetically; + var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500); + + for (var sid in collapsedSections) { + for (var i = 0; i < newSections.length; i++) { + if (newSections[i].id === sid) { + newSections[i].collapsed = collapsedSections[sid]; + } + } + } + + _applyHighlights(newSections, triggerMatch.query); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + + isSearching = false; + searchCompleted(); + return; + } + + activePluginId = ""; + activePluginName = ""; + activePluginCategories = []; + activePluginCategory = ""; + clearActivePluginViewPreference(); + + if (searchMode === "files") { + var fileQuery = searchQuery.startsWith("/") ? searchQuery.substring(1).trim() : searchQuery.trim(); + isFileSearching = fileQuery.length >= 2 && DSearchService.dsearchAvailable; + sections = []; + flatModel = []; + selectedFlatIndex = 0; + selectedItem = null; + isSearching = false; + searchCompleted(); + return; + } + + if (searchMode === "apps") { + var isCategoryFiltered = appCategory && appCategory !== I18n.tr("All"); + var cachedSections = AppSearchService.getCachedDefaultSections(); + if (cachedSections && !searchQuery && !isCategoryFiltered) { + var modeCache = _getCachedModeData("apps"); + if (modeCache) { + _applyHighlights(modeCache.sections, ""); + sections = modeCache.sections; + flatModel = modeCache.flatModel; + } else { + var appSectionIds = ["favorites", "apps"]; + var newSections = cachedSections.filter(function (s) { + return appSectionIds.indexOf(s.id) !== -1; + }).map(function (s) { + var copy = Object.assign({}, s, { + items: s.items ? s.items.slice() : [] + }); + if (collapsedSections[s.id] !== undefined) + copy.collapsed = collapsedSections[s.id]; + return copy; + }); + _applyHighlights(newSections, ""); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + _setCachedModeData("apps", sections, flatModel); + } + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + isSearching = false; + searchCompleted(); + return; + } + + if (isCategoryFiltered) { + var rawApps = AppSearchService.getAppsInCategory(appCategory); + for (var i = 0; i < rawApps.length; i++) { + allItems.push(getOrTransformApp(rawApps[i])); + } + // Also include core apps (DMS Settings etc.) that match this category + var allCoreApps = AppSearchService.getCoreApps(""); + for (var i = 0; i < allCoreApps.length; i++) { + var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]); + if (coreAppCats.indexOf(appCategory) !== -1) + allItems.push(transformCoreApp(allCoreApps[i])); + } + } else { + var apps = searchApps(searchQuery); + for (var i = 0; i < apps.length; i++) { + allItems.push(apps[i]); + } + } + + var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); + var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; + var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500); + + for (var sid in collapsedSections) { + for (var i = 0; i < newSections.length; i++) { + if (newSections[i].id === sid) { + newSections[i].collapsed = collapsedSections[sid]; + } + } + } + + _applyHighlights(newSections, searchQuery); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + + isSearching = false; + searchCompleted(); + return; + } + + if (searchMode === "plugins") { + if (!searchQuery && !pluginFilter) { + var browseItems = getPluginBrowseItems(); + for (var k = 0; k < browseItems.length; k++) + allItems.push(browseItems[k]); + } else if (pluginFilter) { + var isBuiltInFilter = !!AppSearchService.builtInPlugins[pluginFilter]; + applyActivePluginViewPreference(pluginFilter, isBuiltInFilter); + + var filterItems = getPluginItems(pluginFilter, searchQuery); + for (var k = 0; k < filterItems.length; k++) + allItems.push(filterItems[k]); + + var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery); + for (var j = 0; j < builtInItems.length; j++) { + allItems.push(transformBuiltInLauncherItem(builtInItems[j], pluginFilter)); + } + } else { + var emptyTriggerPlugins = getEmptyTriggerPlugins(); + for (var i = 0; i < emptyTriggerPlugins.length; i++) { + var pluginId = emptyTriggerPlugins[i]; + var pItems = getPluginItems(pluginId, searchQuery); + for (var k = 0; k < pItems.length; k++) + allItems.push(pItems[k]); + } + + var builtInLauncherPlugins = getBuiltInEmptyTriggerLaunchers(); + for (var i = 0; i < builtInLauncherPlugins.length; i++) { + var pluginId = builtInLauncherPlugins[i]; + var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery); + for (var j = 0; j < blItems.length; j++) { + allItems.push(transformBuiltInLauncherItem(blItems[j], pluginId)); + } + } + } + + var dynamicDefs = buildDynamicSectionDefs(allItems); + var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); + var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; + var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500); + + for (var sid in collapsedSections) { + for (var i = 0; i < newSections.length; i++) { + if (newSections[i].id === sid) { + newSections[i].collapsed = collapsedSections[sid]; + } + } + } + + _applyHighlights(newSections, searchQuery); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + + isSearching = false; + searchCompleted(); + return; + } + + var apps = searchApps(searchQuery); + for (var i = 0; i < apps.length; i++) { + allItems.push(apps[i]); + } + + if (searchMode === "all") { + if (searchQuery && searchQuery.length >= 2) { + _pluginPhasePending = true; + _phase1Items = allItems.slice(); + _pluginPhaseForceFirst = shouldResetSelection; + pluginPhaseTimer.restart(); + isSearching = true; + searchCompleted(); + return; + } else if (!searchQuery) { + var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered(); + for (var i = 0; i < emptyTriggerOrdered.length; i++) { + var plugin = emptyTriggerOrdered[i]; + if (plugin.isBuiltIn) { + var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); + for (var j = 0; j < blItems.length; j++) + allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id)); + } else { + var pItems = getPluginItems(plugin.id, searchQuery); + for (var j = 0; j < pItems.length; j++) + allItems.push(pItems[j]); + } + } + + var browseItems = getPluginBrowseItems(); + for (var i = 0; i < browseItems.length; i++) + allItems.push(browseItems[i]); + } + } + + var dynamicDefs = buildDynamicSectionDefs(allItems); + + if (currentVersion !== _searchVersion) { + isSearching = false; + return; + } + + var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); + var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; + var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, searchQuery ? 50 : 500); + + if (currentVersion !== _searchVersion) { + isSearching = false; + return; + } + + for (var i = 0; i < newSections.length; i++) { + var sid = newSections[i].id; + if (collapsedSections[sid] !== undefined) { + newSections[i].collapsed = collapsedSections[sid]; + } + } + + _applyHighlights(newSections, searchQuery); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + + if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) { + AppSearchService.setCachedDefaultSections(sections, flatModel); + _saveDiskCache(sections); + } + + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + + isSearching = _pluginPhasePending; + searchCompleted(); + } + + function _performPluginPhase() { + _pluginPhasePending = false; + if (!searchQuery || searchQuery.length < 2 || searchMode !== "all") + return; + + var currentVersion = _searchVersion; + var restoreSelection = preserveSelectionAfterUpdate(_pluginPhaseForceFirst); + var allItems = _phase1Items; + _phase1Items = []; + + var allPluginsOrdered = getAllVisiblePluginsOrdered(); + var maxPerPlugin = 10; + for (var i = 0; i < allPluginsOrdered.length; i++) { + if (currentVersion !== _searchVersion) + return; + var plugin = allPluginsOrdered[i]; + if (plugin.isBuiltIn) { + var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); + var blLimit = Math.min(blItems.length, maxPerPlugin); + for (var j = 0; j < blLimit; j++) { + var item = transformBuiltInLauncherItem(blItems[j], plugin.id); + item._preScored = 900 - j; + allItems.push(item); + } + } else { + var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin); + for (var j = 0; j < pItems.length; j++) { + pItems[j]._preScored = 900 - j; + allItems.push(pItems[j]); + } + } + } + + if (currentVersion !== _searchVersion) + return; + + var dynamicDefs = buildDynamicSectionDefs(allItems); + var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); + var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, false, 50); + + if (currentVersion !== _searchVersion) + return; + + for (var i = 0; i < newSections.length; i++) { + var sid = newSections[i].id; + if (collapsedSections[sid] !== undefined) + newSections[i].collapsed = collapsedSections[sid]; + } + + _applyHighlights(newSections, searchQuery); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + isSearching = false; + searchCompleted(); + } + + function performFileSearch() { + if (!DSearchService.dsearchAvailable) + return; + var fileQuery = ""; + var effectiveType = fileSearchType || "all"; + var includeFiles = SettingsData.dankLauncherV2IncludeFilesInAll; + var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll; + + if (searchQuery.startsWith("/")) { + fileQuery = searchQuery.substring(1).trim(); + } else if (searchMode === "files") { + fileQuery = searchQuery.trim(); + } else if (searchMode === "all" && (includeFiles || includeFolders)) { + fileQuery = searchQuery.trim(); + if (includeFiles && !includeFolders) + effectiveType = "file"; + else if (!includeFiles && includeFolders) + effectiveType = "dir"; + else + effectiveType = "all"; + } else { + return; + } + + if (fileQuery.length < 2) { + isFileSearching = false; + return; + } + + isFileSearching = true; + + var splitBothTypes = searchMode === "all" && includeFiles && includeFolders && DSearchService.supportsTypeFilter; + var queryTypes = splitBothTypes ? ["file", "dir"] : [effectiveType]; + var pending = queryTypes.length; + var aggregatedItems = []; + + for (var t = 0; t < queryTypes.length; t++) { + var queryType = queryTypes[t]; + var params = { + limit: 20, + fuzzy: true, + sort: fileSearchSort || "score", + desc: true + }; + + if (DSearchService.supportsTypeFilter) { + params.type = (queryType && queryType !== "all") ? queryType : "all"; + } + if (fileSearchExt) { + params.ext = fileSearchExt; + } + if (fileSearchFolder) { + params.folder = fileSearchFolder; + } + + DSearchService.search(fileQuery, params, function (response) { + pending--; + if (!response.error) { + var hits = response.result?.hits || []; + for (var i = 0; i < hits.length; i++) { + var hit = hits[i]; + var docTypes = hit.locations?.doc_type; + var isDir = docTypes ? !!docTypes["dir"] : false; + aggregatedItems.push(transformFileResult({ + path: hit.id || "", + score: hit.score || 0, + is_dir: isDir + })); + } + } + if (pending > 0) + return; + + isFileSearching = false; + _applyFileSearchResults(aggregatedItems, effectiveType); + }); + } + } + + function _applyFileSearchResults(fileItems, effectiveType) { + var fileSections = []; + var showType = effectiveType; + var order = SettingsData.launcherPluginOrder || []; + var filesOrderIdx = order.indexOf("__files"); + var foldersOrderIdx = order.indexOf("__folders"); + var filesPriority = filesOrderIdx !== -1 ? 2.6 + filesOrderIdx * 0.01 : 4; + var foldersPriority = foldersOrderIdx !== -1 ? 2.6 + foldersOrderIdx * 0.01 : 4.1; + + if (showType === "all" && DSearchService.supportsTypeFilter) { + var onlyFiles = []; + var onlyDirs = []; + for (var j = 0; j < fileItems.length; j++) { + if (fileItems[j].data?.is_dir) + onlyDirs.push(fileItems[j]); + else + onlyFiles.push(fileItems[j]); + } + if (onlyFiles.length > 0) { + fileSections.push({ + id: "files", + title: I18n.tr("Files"), + icon: "insert_drive_file", + priority: filesPriority, + items: onlyFiles, + collapsed: collapsedSections["files"] || false, + flatStartIndex: 0 + }); + } + if (onlyDirs.length > 0) { + fileSections.push({ + id: "folders", + title: I18n.tr("Folders"), + icon: "folder", + priority: foldersPriority, + items: onlyDirs, + collapsed: collapsedSections["folders"] || false, + flatStartIndex: 0 + }); + } + } else { + var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder"; + var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files"); + var singlePriority = showType === "dir" ? foldersPriority : filesPriority; + if (fileItems.length > 0) { + fileSections.push({ + id: "files", + title: filesTitle, + icon: filesIcon, + priority: singlePriority, + items: fileItems, + collapsed: collapsedSections["files"] || false, + flatStartIndex: 0 + }); + } + } + + var newSections; + if (searchMode === "files") { + newSections = fileSections; + } else { + var existingNonFile = sections.filter(function (s) { + return s.id !== "files" && s.id !== "folders"; + }); + newSections = existingNonFile.concat(fileSections); + } + newSections.sort(function (a, b) { + return a.priority - b.priority; + }); + _applyHighlights(newSections, searchQuery); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + selectedFlatIndex = getFirstItemIndex(); + updateSelectedItem(); + } + + function searchApps(query) { + var apps = AppSearchService.searchApplications(query); + var items = []; + + for (var i = 0; i < apps.length; i++) { + items.push(getOrTransformApp(apps[i])); + } + + var coreApps = AppSearchService.getCoreApps(query); + for (var i = 0; i < coreApps.length; i++) { + items.push(transformCoreApp(coreApps[i])); + } + + return items; + } + + function transformApp(app) { + var appId = app.id || app.execString || app.exec || ""; + var override = SessionData.getAppOverride(appId); + return Transform.transformApp(app, override, [], I18n.tr("Launch")); + } + + function transformCoreApp(app) { + return Transform.transformCoreApp(app, I18n.tr("Open")); + } + + function transformBuiltInLauncherItem(item, pluginId) { + return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open")); + } + + function transformFileResult(file) { + return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal")); + } + + function detectTrigger(query) { + if (!query || query.length === 0) + return { + pluginId: null, + query: query + }; + + var pluginTriggers = PluginService.getAllPluginTriggers(); + for (var trigger in pluginTriggers) { + if (trigger && query.startsWith(trigger)) { + return { + pluginId: pluginTriggers[trigger], + query: query.substring(trigger.length).trim() + }; + } + } + + var builtInTriggers = AppSearchService.getBuiltInLauncherTriggers(); + for (var trigger in builtInTriggers) { + if (trigger && query.startsWith(trigger)) { + return { + pluginId: builtInTriggers[trigger], + query: query.substring(trigger.length).trim(), + isBuiltIn: true + }; + } + } + + return { + pluginId: null, + query: query + }; + } + + function getEmptyTriggerPlugins() { + var plugins = PluginService.getPluginsWithEmptyTrigger(); + var visible = plugins.filter(function (pluginId) { + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + }); + return sortPluginIdsByOrder(visible); + } + + function getAllLauncherPluginIds() { + var launchers = PluginService.getLauncherPlugins(); + return Object.keys(launchers); + } + + function getVisibleLauncherPluginIds() { + var launchers = PluginService.getLauncherPlugins(); + var visible = Object.keys(launchers).filter(function (pluginId) { + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + }); + return sortPluginIdsByOrder(visible); + } + + function getAllBuiltInLauncherIds() { + var launchers = AppSearchService.getBuiltInLauncherPlugins(); + return Object.keys(launchers); + } + + function getVisibleBuiltInLauncherIds() { + var launchers = AppSearchService.getBuiltInLauncherPlugins(); + var visible = Object.keys(launchers).filter(function (pluginId) { + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + }); + return sortPluginIdsByOrder(visible); + } + + function sortPluginIdsByOrder(pluginIds) { + return Utils.sortPluginIdsByOrder(pluginIds, SettingsData.launcherPluginOrder || []); + } + + function getAllVisiblePluginsOrdered() { + var thirdPartyLaunchers = PluginService.getLauncherPlugins() || {}; + var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins() || {}; + var all = []; + for (var id in thirdPartyLaunchers) { + if (SettingsData.getPluginAllowWithoutTrigger(id)) + all.push({ + id: id, + isBuiltIn: false + }); + } + for (var id in builtInLaunchers) { + if (SettingsData.getPluginAllowWithoutTrigger(id)) + all.push({ + id: id, + isBuiltIn: true + }); + } + return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []); + } + + function getEmptyTriggerPluginsOrdered() { + var thirdParty = PluginService.getPluginsWithEmptyTrigger() || []; + var builtIn = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger() || []; + var all = []; + for (var i = 0; i < thirdParty.length; i++) { + var id = thirdParty[i]; + if (SettingsData.getPluginAllowWithoutTrigger(id)) + all.push({ + id: id, + isBuiltIn: false + }); + } + for (var i = 0; i < builtIn.length; i++) { + var id = builtIn[i]; + if (SettingsData.getPluginAllowWithoutTrigger(id)) + all.push({ + id: id, + isBuiltIn: true + }); + } + return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []); + } + + function getPluginBrowseItems() { + var items = []; + var browseLabel = I18n.tr("Browse"); + var triggerLabel = I18n.tr("Trigger: %1"); + var noTriggerLabel = I18n.tr("No trigger"); + + var launchers = PluginService.getLauncherPlugins(); + for (var pluginId in launchers) { + var trigger = PluginService.getPluginTrigger(pluginId); + var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId); + items.push(Transform.createPluginBrowseItem(pluginId, launchers[pluginId], trigger, false, isAllowed, browseLabel, triggerLabel, noTriggerLabel)); + } + + var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins(); + for (var pluginId in builtInLaunchers) { + var trigger = AppSearchService.getBuiltInPluginTrigger(pluginId); + var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId); + items.push(Transform.createPluginBrowseItem(pluginId, builtInLaunchers[pluginId], trigger, true, isAllowed, browseLabel, triggerLabel, noTriggerLabel)); + } + + return items; + } + + function getBuiltInEmptyTriggerLaunchers() { + var plugins = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger(); + var visible = plugins.filter(function (pluginId) { + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + }); + return sortPluginIdsByOrder(visible); + } + + function getPluginItems(pluginId, query, limit) { + var items = AppSearchService.getPluginItemsForPlugin(pluginId, query); + var count = limit > 0 && limit < items.length ? limit : items.length; + var transformed = []; + + for (var i = 0; i < count; i++) { + transformed.push(transformPluginItem(items[i], pluginId)); + } + + return transformed; + } + + function getPluginName(pluginId, isBuiltIn) { + if (isBuiltIn) { + var plugin = AppSearchService.builtInPlugins[pluginId]; + return plugin ? plugin.name : pluginId; + } + var launchers = PluginService.getLauncherPlugins(); + if (launchers[pluginId]) { + return launchers[pluginId].name || pluginId; + } + return pluginId; + } + + function getPluginMetadata(pluginId) { + var builtIn = AppSearchService.builtInPlugins[pluginId]; + if (builtIn) { + return { + name: builtIn.name || pluginId, + icon: builtIn.cornerIcon || "extension" + }; + } + var launchers = PluginService.getLauncherPlugins(); + if (launchers[pluginId]) { + var rawIcon = launchers[pluginId].icon || "extension"; + return { + name: launchers[pluginId].name || pluginId, + icon: Utils.stripIconPrefix(rawIcon) + }; + } + return { + name: pluginId, + icon: "extension" + }; + } + + function buildDynamicSectionDefs(items) { + var baseDefs = sectionDefinitions.slice(); + var pluginSections = {}; + var order = SettingsData.launcherPluginOrder || []; + var orderMap = {}; + for (var k = 0; k < order.length; k++) + orderMap[order[k]] = k; + var unorderedPriority = 2.6 + order.length * 0.01; + + for (var i = 0; i < items.length; i++) { + var section = items[i].section; + if (!section || !section.startsWith("plugin_")) + continue; + if (pluginSections[section]) + continue; + var pluginId = section.substring(7); + var meta = getPluginMetadata(pluginId); + var viewPref = getPluginViewPref(pluginId); + var orderIdx = orderMap[pluginId]; + var priority; + if (orderIdx !== undefined) { + priority = 2.6 + orderIdx * 0.01; + } else { + priority = unorderedPriority; + unorderedPriority += 0.01; + } + + pluginSections[section] = { + id: section, + title: meta.name, + icon: meta.icon, + priority: priority, + defaultViewMode: viewPref.mode || "list" + }; + + if (viewPref.mode) + setPluginViewPreference(section, viewPref.mode, viewPref.enforced); + } + + for (var sectionId in pluginSections) { + baseDefs.push(pluginSections[sectionId]); + } + + baseDefs.sort(function (a, b) { + return a.priority - b.priority; + }); + return baseDefs; + } + + function getPluginViewPref(pluginId) { + var builtIn = AppSearchService.builtInPlugins[pluginId]; + if (builtIn && builtIn.viewMode) { + return { + mode: builtIn.viewMode, + enforced: builtIn.viewModeEnforced === true + }; + } + + var pref = PluginService.getPluginViewPreference(pluginId); + if (pref && pref.mode) { + return pref; + } + + return { + mode: "list", + enforced: false + }; + } + + function transformPluginItem(item, pluginId) { + return Transform.transformPluginItem(item, pluginId, I18n.tr("Select")); + } + + function getFrecencyForItem(item) { + if (item.type !== "app") + return null; + + var appId = item.id; + var usageRanking = AppUsageHistoryData.appUsageRanking || {}; + + var idVariants = [appId, appId.replace(".desktop", "")]; + var usageData = null; + + for (var i = 0; i < idVariants.length; i++) { + if (usageRanking[idVariants[i]]) { + usageData = usageRanking[idVariants[i]]; + break; + } + } + + return { + usageCount: usageData?.usageCount || 0 + }; + } + + function getFirstItemIndex() { + return Nav.getFirstItemIndex(flatModel); + } + + function _getCachedModeData(mode) { + return _modeSectionsCache[mode] || null; + } + + function _setCachedModeData(mode, sectionsData, flatModelData) { + var cache = Object.assign({}, _modeSectionsCache); + cache[mode] = { + sections: sectionsData, + flatModel: flatModelData + }; + _modeSectionsCache = cache; + } + + function _clearModeCache() { + _modeSectionsCache = {}; + } + + function _saveDiskCache(sectionsData) { + var serializable = []; + for (var i = 0; i < sectionsData.length; i++) { + var s = sectionsData[i]; + var items = []; + var srcItems = s.items || []; + for (var j = 0; j < srcItems.length; j++) { + var it = srcItems[j]; + items.push({ + id: it.id, + type: it.type, + name: it.name || "", + subtitle: it.subtitle || "", + icon: it.icon || "", + iconType: it.iconType || "image", + iconFull: it.iconFull || "", + section: it.section || "", + isCore: it.isCore || false, + isBuiltInLauncher: it.isBuiltInLauncher || false, + pluginId: it.pluginId || "" + }); + } + serializable.push({ + id: s.id, + title: s.title || "", + icon: s.icon || "", + priority: s.priority || 0, + items: items + }); + } + CacheData.saveLauncherCache(serializable); + } + + function _actionsFromDesktopEntry(appId) { + if (!appId) + return []; + var entry = DesktopEntries.heuristicLookup(appId); + if (!entry || !entry.actions || entry.actions.length === 0) + return []; + var result = []; + for (var i = 0; i < entry.actions.length; i++) { + result.push({ + name: entry.actions[i].name, + icon: "play_arrow", + actionData: entry.actions[i] + }); + } + return result; + } + + function _loadDiskCache() { + var cached = CacheData.loadLauncherCache(); + if (!cached || !Array.isArray(cached) || cached.length === 0) + return null; + + var sectionsData = []; + for (var i = 0; i < cached.length; i++) { + var s = cached[i]; + var items = []; + var srcItems = s.items || []; + for (var j = 0; j < srcItems.length; j++) { + var it = srcItems[j]; + items.push({ + id: it.id || "", + type: it.type || "app", + name: it.name || "", + subtitle: it.subtitle || "", + icon: it.icon || "", + iconType: it.iconType || "image", + iconFull: it.iconFull || "", + section: it.section || "", + isCore: it.isCore || false, + isBuiltInLauncher: it.isBuiltInLauncher || false, + pluginId: it.pluginId || "", + data: { + id: it.id + }, + actions: _actionsFromDesktopEntry(it.id), + primaryAction: it.type === "app" && !it.isCore ? { + name: I18n.tr("Launch"), + icon: "open_in_new", + action: "launch" + } : null, + _diskCached: true, + _hName: "", + _hSub: "", + _hRich: false, + _preScored: undefined + }); + } + sectionsData.push({ + id: s.id || "", + title: s.title || "", + icon: s.icon || "", + priority: s.priority || 0, + items: items, + collapsed: false, + flatStartIndex: 0 + }); + } + return sectionsData; + } + + function updateSelectedItem() { + if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) { + var entry = flatModel[selectedFlatIndex]; + selectedItem = entry.isHeader ? null : entry.item; + } else { + selectedItem = null; + } + } + + function _applyHighlights(sectionsData, query) { + if (!query || query.length === 0) { + for (var i = 0; i < sectionsData.length; i++) { + var items = sectionsData[i].items; + for (var j = 0; j < items.length; j++) { + var item = items[j]; + item._hName = item.name || ""; + item._hSub = item.subtitle || ""; + item._hRich = false; + } + } + return; + } + + var highlightColor = Theme.primary; + var nameColor = Theme.surfaceText; + var subColor = Theme.surfaceVariantText; + var lowerQuery = query.toLowerCase(); + + for (var i = 0; i < sectionsData.length; i++) { + var items = sectionsData[i].items; + for (var j = 0; j < items.length; j++) { + var item = items[j]; + item._hName = _highlightField(item.name || "", lowerQuery, query.length, nameColor, highlightColor); + item._hSub = _highlightField(item.subtitle || "", lowerQuery, query.length, subColor, highlightColor); + item._hRich = true; + } + } + } + + function _highlightField(text, lowerQuery, queryLen, baseColor, highlightColor) { + if (!text) + return ""; + var idx = text.toLowerCase().indexOf(lowerQuery); + if (idx === -1) + return text; + var before = text.substring(0, idx); + var match = text.substring(idx, idx + queryLen); + var after = text.substring(idx + queryLen); + return '' + before + '' + match + '' + after + ''; + } + + function getCurrentSectionViewMode() { + if (selectedFlatIndex < 0 || selectedFlatIndex >= flatModel.length) + return "list"; + var entry = flatModel[selectedFlatIndex]; + if (!entry || entry.isHeader) + return "list"; + return getSectionViewMode(entry.sectionId); + } + + function getGridColumns(sectionId) { + return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns); + } + + function _cancelPendingSelectionReset() { + _queryDrivenSearch = false; + _pluginPhaseForceFirst = false; + } + + function selectNext() { + keyboardNavigationActive = true; + _cancelPendingSelectionReset(); + var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); + } + } + + function selectPrevious() { + keyboardNavigationActive = true; + _cancelPendingSelectionReset(); + var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); + } + } + + function selectRight() { + keyboardNavigationActive = true; + _cancelPendingSelectionReset(); + var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); + } + } + + function selectLeft() { + keyboardNavigationActive = true; + _cancelPendingSelectionReset(); + var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); + } + } + + function selectNextSection() { + keyboardNavigationActive = true; + _cancelPendingSelectionReset(); + var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); + } + } + + function selectPreviousSection() { + keyboardNavigationActive = true; + _cancelPendingSelectionReset(); + var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); + } + } + + function selectPageDown(visibleItems) { + keyboardNavigationActive = true; + _cancelPendingSelectionReset(); + var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); + } + } + + function selectPageUp(visibleItems) { + keyboardNavigationActive = true; + _cancelPendingSelectionReset(); + var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); + } + } + + function selectIndex(index) { + keyboardNavigationActive = false; + if (index >= 0 && index < flatModel.length && !flatModel[index].isHeader) { + selectedFlatIndex = index; + updateSelectedItem(); + } + } + + function toggleSection(sectionId) { + _clearModeCache(); + var newCollapsed = Object.assign({}, collapsedSections); + var currentState = newCollapsed[sectionId]; + + if (currentState === undefined) { + for (var i = 0; i < sections.length; i++) { + if (sections[i].id === sectionId) { + currentState = sections[i].collapsed || false; + break; + } + } + } + + newCollapsed[sectionId] = !currentState; + collapsedSections = newCollapsed; + + var newSections = sections.slice(); + for (var i = 0; i < newSections.length; i++) { + if (newSections[i].id === sectionId) { + newSections[i] = Object.assign({}, newSections[i], { + collapsed: newCollapsed[sectionId] + }); + } + } + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + + if (selectedFlatIndex >= flatModel.length) { + selectedFlatIndex = getFirstItemIndex(); + } + updateSelectedItem(); + } + + function executeSelected() { + if (searchDebounce.running) { + searchDebounce.stop(); + performSearch(); + } + if (!selectedItem) + return; + executeItem(selectedItem); + } + + function executeItem(item) { + if (!item) + return; + + SessionData.addLauncherHistory(searchQuery); + + if (item.type === "plugin_browse") { + var browsePluginId = item.data?.pluginId; + if (!browsePluginId) + return; + var browseTrigger = item.data.isBuiltIn ? AppSearchService.getBuiltInPluginTrigger(browsePluginId) : PluginService.getPluginTrigger(browsePluginId); + + if (browseTrigger && browseTrigger.length > 0) { + searchQueryRequested(browseTrigger); + } else { + setMode("plugins"); + pluginFilter = browsePluginId; + performSearch(); + } + return; + } + + switch (item.type) { + case "app": + if (item.isCore) { + AppSearchService.executeCoreApp(item.data); + } else if (item.data?.isAction) { + launchAppAction(item.data); + } else { + launchApp(item.data); + } + break; + case "plugin": + if (item.isBuiltInLauncher) { + AppSearchService.executeBuiltInLauncherItem(item.data); + } else { + AppSearchService.executePluginItem(item.data, item.pluginId); + } + break; + case "file": + openFile(item.data?.path); + break; + default: + return; + } + + itemExecuted(); + } + + function executeAction(item, action) { + if (!item || !action) + return; + switch (action.action) { + case "launch": + executeItem(item); + break; + case "open": + openFile(item.data.path); + break; + case "open_folder": + openFolder(item.data.path); + break; + case "copy_path": + copyToClipboard(item.data.path); + break; + case "open_terminal": + openTerminal(item.data.path); + break; + case "copy": + copyToClipboard(item.name); + break; + case "execute": + executeItem(item); + break; + case "launch_dgpu": + if (item.type === "app" && item.data) { + launchAppWithNvidia(item.data); + } + break; + case "toggle_all_visibility": + if (item.type === "plugin_browse" && item.data?.pluginId) { + var pluginId = item.data.pluginId; + var currentState = SettingsData.getPluginAllowWithoutTrigger(pluginId); + SettingsData.setPluginAllowWithoutTrigger(pluginId, !currentState); + performSearch(); + } + return; + default: + if (item.type === "app" && action.actionData) { + launchAppAction({ + parentApp: item.data, + actionData: action.actionData + }); + } + } + + itemExecuted(); + } + + function _resolveDesktopEntry(app) { + if (!app) + return null; + if (app.command) + return app; + var id = app.id || app.execString || app.exec || ""; + if (!id) + return null; + return DesktopEntries.heuristicLookup(id); + } + + function launchApp(app) { + var entry = _resolveDesktopEntry(app); + if (!entry) + return; + SessionService.launchDesktopEntry(entry); + AppUsageHistoryData.addAppUsage(entry); + } + + function launchAppWithNvidia(app) { + var entry = _resolveDesktopEntry(app); + if (!entry) + return; + SessionService.launchDesktopEntry(entry, true); + AppUsageHistoryData.addAppUsage(entry); + } + + function launchAppAction(actionItem) { + if (!actionItem || !actionItem.actionData) + return; + var entry = _resolveDesktopEntry(actionItem.parentApp); + if (!entry) + return; + SessionService.launchDesktopAction(entry, actionItem.actionData); + AppUsageHistoryData.addAppUsage(entry); + } + + function openFile(path) { + if (!path) + return; + Qt.openUrlExternally("file://" + path); + } + + function openFolder(path) { + if (!path) + return; + var folder = path.substring(0, path.lastIndexOf("/")); + Qt.openUrlExternally("file://" + folder); + } + + function openTerminal(path) { + if (!path) + return; + var terminal = Quickshell.env("TERMINAL") || "xterm"; + Quickshell.execDetached({ + command: [terminal], + workingDirectory: path + }); + } + + function copyToClipboard(text) { + if (!text) + return; + Quickshell.execDetached(["dms", "cl", "copy", text]); + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ControllerUtils.js b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ControllerUtils.js new file mode 100644 index 0000000..a06285f --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ControllerUtils.js @@ -0,0 +1,128 @@ +.pragma library + +function getFileIcon(filename) { + var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : ""; + + switch (ext) { + case "pdf": + return "picture_as_pdf"; + case "doc": + case "docx": + case "odt": + return "description"; + case "xls": + case "xlsx": + case "ods": + return "table_chart"; + case "ppt": + case "pptx": + case "odp": + return "slideshow"; + case "txt": + case "md": + case "rst": + return "article"; + case "jpg": + case "jpeg": + case "png": + case "gif": + case "svg": + case "webp": + return "image"; + case "mp3": + case "wav": + case "flac": + case "ogg": + return "audio_file"; + case "mp4": + case "mkv": + case "avi": + case "webm": + return "video_file"; + case "zip": + case "tar": + case "gz": + case "7z": + case "rar": + return "folder_zip"; + case "js": + case "ts": + case "py": + case "rs": + case "go": + case "java": + case "c": + case "cpp": + case "h": + return "code"; + case "html": + case "css": + case "htm": + return "web"; + case "json": + case "xml": + case "yaml": + case "yml": + return "data_object"; + case "sh": + case "bash": + case "zsh": + return "terminal"; + default: + return "insert_drive_file"; + } +} + +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:")) + return iconName.substring(6); + return iconName; +} + +function detectIconType(iconName) { + if (!iconName) + return "material"; + 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"; +} + +function sortPluginIdsByOrder(pluginIds, order) { + if (!order || order.length === 0) + return pluginIds; + var orderMap = {}; + for (var i = 0; i < order.length; i++) + orderMap[order[i]] = i; + return pluginIds.slice().sort(function (a, b) { + var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999; + var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999; + return aOrder - bOrder; + }); +} + +function sortPluginsOrdered(plugins, order) { + if (!order || order.length === 0) + return plugins; + var orderMap = {}; + for (var i = 0; i < order.length; i++) + orderMap[order[i]] = i; + return plugins.sort(function (a, b) { + var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999; + var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999; + return aOrder - bOrder; + }); +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml new file mode 100644 index 0000000..859022c --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -0,0 +1,465 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + visible: false + + property bool spotlightOpen: false + property bool keyboardActive: false + property bool contentVisible: false + property var spotlightContent: launcherContentLoader.item + property bool openedFromOverview: false + property bool isClosing: false + property bool _pendingInitialize: false + property string _pendingQuery: "" + property string _pendingMode: "" + readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose + + readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab + readonly property var effectiveScreen: launcherWindow.screen + readonly property real screenWidth: effectiveScreen?.width ?? 1920 + readonly property real screenHeight: effectiveScreen?.height ?? 1080 + readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 + + readonly property int baseWidth: { + switch (SettingsData.dankLauncherV2Size) { + case "micro": + return 500; + case "medium": + return 720; + case "large": + return 860; + default: + return 620; + } + } + readonly property int baseHeight: { + switch (SettingsData.dankLauncherV2Size) { + case "micro": + return 480; + case "medium": + return 720; + case "large": + return 860; + default: + return 600; + } + } + readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100) + readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100) + readonly property real modalX: (screenWidth - modalWidth) / 2 + readonly property real modalY: (screenHeight - modalHeight) / 2 + + readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + readonly property real cornerRadius: Theme.cornerRadius + readonly property color borderColor: { + if (!SettingsData.dankLauncherV2BorderEnabled) + return Theme.outlineMedium; + switch (SettingsData.dankLauncherV2BorderColor) { + case "primary": + return Theme.primary; + case "secondary": + return Theme.secondary; + case "outline": + return Theme.outline; + case "surfaceText": + return Theme.surfaceText; + default: + return Theme.primary; + } + } + readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 + + signal dialogClosed + + function _ensureContentLoadedAndInitialize(query, mode) { + _pendingQuery = query || ""; + _pendingMode = mode || ""; + _pendingInitialize = true; + contentVisible = true; + launcherContentLoader.active = true; + + if (spotlightContent) { + _initializeAndShow(_pendingQuery, _pendingMode); + _pendingInitialize = false; + } + } + + function _initializeAndShow(query, mode) { + if (!spotlightContent) + return; + contentVisible = true; + spotlightContent.searchField.forceActiveFocus(); + + var targetQuery = ""; + + if (query) { + targetQuery = query; + } else if (SettingsData.rememberLastQuery) { + targetQuery = SessionData.launcherLastQuery || ""; + } + + if (spotlightContent.searchField) { + spotlightContent.searchField.text = targetQuery; + } + if (spotlightContent.controller) { + var targetMode = mode || SessionData.launcherLastMode || "all"; + spotlightContent.controller.searchMode = targetMode; + spotlightContent.controller.activePluginId = ""; + spotlightContent.controller.activePluginName = ""; + spotlightContent.controller.pluginFilter = ""; + spotlightContent.controller.fileSearchType = "all"; + spotlightContent.controller.fileSearchExt = ""; + spotlightContent.controller.fileSearchFolder = ""; + spotlightContent.controller.fileSearchSort = "score"; + spotlightContent.controller.collapsedSections = {}; + spotlightContent.controller.selectedFlatIndex = 0; + spotlightContent.controller.selectedItem = null; + spotlightContent.controller.historyIndex = -1; + spotlightContent.controller.searchQuery = targetQuery; + + spotlightContent.controller.performSearch(); + } + if (spotlightContent.resetScroll) { + spotlightContent.resetScroll(); + } + if (spotlightContent.actionPanel) { + spotlightContent.actionPanel.hide(); + } + } + + function _finishShow(query, mode) { + spotlightOpen = true; + isClosing = false; + openedFromOverview = false; + + keyboardActive = true; + ModalManager.openModal(root); + if (useHyprlandFocusGrab) + focusGrab.active = true; + + _ensureContentLoadedAndInitialize(query || "", mode || ""); + } + + function show() { + closeCleanupTimer.stop(); + + var focusedScreen = CompositorService.getFocusedScreen(); + if (focusedScreen && launcherWindow.screen !== focusedScreen) { + spotlightOpen = false; + isClosing = false; + launcherWindow.screen = focusedScreen; + Qt.callLater(() => root._finishShow("", "")); + return; + } + + _finishShow("", ""); + } + + function showWithQuery(query) { + closeCleanupTimer.stop(); + + var focusedScreen = CompositorService.getFocusedScreen(); + if (focusedScreen && launcherWindow.screen !== focusedScreen) { + spotlightOpen = false; + isClosing = false; + launcherWindow.screen = focusedScreen; + Qt.callLater(() => root._finishShow(query, "")); + return; + } + + _finishShow(query, ""); + } + + function hide() { + if (!spotlightOpen) + return; + openedFromOverview = false; + isClosing = true; + contentVisible = false; + + keyboardActive = false; + spotlightOpen = false; + focusGrab.active = false; + ModalManager.closeModal(root); + + closeCleanupTimer.start(); + } + + function toggle() { + spotlightOpen ? hide() : show(); + } + + function showWithMode(mode) { + closeCleanupTimer.stop(); + + var focusedScreen = CompositorService.getFocusedScreen(); + if (focusedScreen && launcherWindow.screen !== focusedScreen) { + spotlightOpen = false; + isClosing = false; + launcherWindow.screen = focusedScreen; + Qt.callLater(() => root._finishShow("", mode)); + return; + } + + spotlightOpen = true; + isClosing = false; + openedFromOverview = false; + + keyboardActive = true; + ModalManager.openModal(root); + if (useHyprlandFocusGrab) + focusGrab.active = true; + + _ensureContentLoadedAndInitialize("", mode); + } + + function toggleWithMode(mode) { + if (spotlightOpen) { + hide(); + } else { + showWithMode(mode); + } + } + + function toggleWithQuery(query) { + if (spotlightOpen) { + hide(); + } else { + showWithQuery(query); + } + } + + Timer { + id: closeCleanupTimer + interval: Theme.modalAnimationDuration + 50 + repeat: false + onTriggered: { + isClosing = false; + if (root.unloadContentOnClose) + launcherContentLoader.active = false; + dialogClosed(); + } + } + + Connections { + target: spotlightContent?.controller ?? null + + function onModeChanged(mode) { + if (spotlightContent.controller.autoSwitchedToFiles) + return; + SessionData.setLauncherLastMode(mode); + } + } + + HyprlandFocusGrab { + id: focusGrab + windows: [launcherWindow] + active: false + + onCleared: { + if (spotlightOpen) { + hide(); + } + } + } + + Connections { + target: ModalManager + function onCloseAllModalsExcept(excludedModal) { + if (excludedModal !== root && spotlightOpen) { + hide(); + } + } + } + + Connections { + target: Quickshell + function onScreensChanged() { + if (Quickshell.screens.length === 0) + return; + + const screenName = launcherWindow.screen?.name; + if (screenName) { + for (let i = 0; i < Quickshell.screens.length; i++) { + if (Quickshell.screens[i].name === screenName) + return; + } + } + + if (spotlightOpen) + hide(); + + const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0]; + if (newScreen) + launcherWindow.screen = newScreen; + } + } + + PanelWindow { + id: launcherWindow + visible: spotlightOpen || isClosing + color: "transparent" + exclusionMode: ExclusionMode.Ignore + + WindowBlur { + targetWindow: launcherWindow + readonly property real s: Math.min(1, modalContainer.scale) + blurX: root.modalX + root.modalWidth * (1 - s) * 0.5 + blurY: root.modalY + root.modalHeight * (1 - s) * 0.5 + blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0 + blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0 + blurRadius: root.cornerRadius + } + + WlrLayershell.namespace: "dms:spotlight" + WlrLayershell.layer: { + switch (Quickshell.env("DMS_MODAL_LAYER")) { + case "bottom": + console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer."); + return WlrLayershell.Top; + case "background": + console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); + return WlrLayershell.Top; + case "overlay": + return WlrLayershell.Overlay; + default: + return WlrLayershell.Top; + } + } + WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None + + anchors { + top: true + bottom: true + left: true + right: true + } + + mask: Region { + item: spotlightOpen ? fullScreenMask : null + } + + Item { + id: fullScreenMask + anchors.fill: parent + } + + Rectangle { + id: backgroundDarken + anchors.fill: parent + color: "black" + opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 + visible: contentVisible || opacity > 0 + + Behavior on opacity { + DankAnim { + duration: Theme.modalAnimationDuration + easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + } + } + } + + MouseArea { + anchors.fill: parent + enabled: spotlightOpen + onClicked: mouse => { + var contentX = modalContainer.x; + var contentY = modalContainer.y; + var contentW = modalContainer.width; + var contentH = modalContainer.height; + + if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) { + root.hide(); + } + } + } + + Item { + id: modalContainer + x: root.modalX + y: root.modalY + width: root.modalWidth + height: root.modalHeight + visible: contentVisible || opacity > 0 + + opacity: contentVisible ? 1 : 0 + scale: contentVisible ? 1 : 0.96 + transformOrigin: Item.Center + + Behavior on opacity { + DankAnim { + duration: Theme.modalAnimationDuration + easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + } + } + + Behavior on scale { + DankAnim { + duration: Theme.modalAnimationDuration + easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + } + } + + ElevationShadow { + id: launcherShadowLayer + anchors.fill: parent + level: Theme.elevationLevel3 + fallbackOffset: 6 + targetColor: root.backgroundColor + borderColor: root.borderColor + borderWidth: root.borderWidth + targetRadius: root.cornerRadius + shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" + } + + MouseArea { + anchors.fill: parent + onPressed: mouse => mouse.accepted = true + } + + FocusScope { + anchors.fill: parent + focus: keyboardActive + + Loader { + id: launcherContentLoader + anchors.fill: parent + active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize + asynchronous: false + sourceComponent: LauncherContent { + focus: true + parentModal: root + } + + onLoaded: { + if (root._pendingInitialize) { + root._initializeAndShow(root._pendingQuery, root._pendingMode); + root._pendingInitialize = false; + } + } + } + + Keys.onEscapePressed: event => { + root.hide(); + event.accepted = true; + } + } + + Rectangle { + anchors.fill: parent + radius: root.cornerRadius + color: "transparent" + border.color: BlurService.borderColor + border.width: BlurService.borderWidth + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/GridItem.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/GridItem.qml new file mode 100644 index 0000000..a7f39c3 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/GridItem.qml @@ -0,0 +1,105 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property var item: null + property bool isSelected: false + property bool isHovered: itemArea.containsMouse + property var controller: null + property int flatIndex: -1 + + signal clicked + signal rightClicked(real mouseX, real mouseY) + + readonly property string iconValue: { + if (!item) + return ""; + switch (item.iconType) { + case "material": + case "nerd": + return "material:" + (item.icon || "apps"); + case "unicode": + return "unicode:" + (item.icon || ""); + case "composite": + return item.iconFull || ""; + case "image": + default: + return item.icon || ""; + } + } + + readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45)) + + radius: Theme.cornerRadius + color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent" + + DankRipple { + id: rippleLayer + rippleColor: Theme.surfaceText + cornerRadius: root.radius + } + + Column { + anchors.centerIn: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + width: parent.width - Theme.spacingM + + AppIconRenderer { + width: root.computedIconSize + height: root.computedIconSize + anchors.horizontalCenter: parent.horizontalCenter + iconValue: root.iconValue + iconSize: root.computedIconSize + fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?" + iconColor: root.isSelected ? Theme.primary : Theme.surfaceText + materialIconSizeAdjustment: root.computedIconSize * 0.3 + } + + Text { + width: parent.width + text: root.item?._hName ?? root.item?.name ?? "" + textFormat: root.item?._hRich ? Text.RichText : Text.PlainText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + font.family: Theme.fontFamily + color: root.isSelected ? Theme.primary : Theme.surfaceText + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 + wrapMode: Text.Wrap + } + } + + MouseArea { + id: itemArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: mouse => { + if (mouse.button === Qt.LeftButton) + rippleLayer.trigger(mouse.x, mouse.y); + } + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + var scenePos = mapToItem(null, mouse.x, mouse.y); + root.rightClicked(scenePos.x, scenePos.y); + } else { + root.clicked(); + } + } + + onPositionChanged: { + if (root.controller) { + root.controller.keyboardNavigationActive = false; + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ItemTransformers.js b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ItemTransformers.js new file mode 100644 index 0000000..61c108c --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ItemTransformers.js @@ -0,0 +1,237 @@ +.pragma library + + .import "ControllerUtils.js" as Utils + +function transformApp(app, override, defaultActions, primaryActionLabel) { + var appId = app.id || app.execString || app.exec || ""; + + var actions = []; + if (app.actions && app.actions.length > 0) { + for (var i = 0; i < app.actions.length; i++) { + actions.push({ + name: app.actions[i].name, + icon: "play_arrow", + actionData: app.actions[i] + }); + } + } + + return { + id: appId, + type: "app", + name: override?.name || app.name || "", + subtitle: override?.comment || app.comment || "", + icon: override?.icon || app.icon || "application-x-executable", + iconType: "image", + section: "apps", + data: app, + keywords: app.keywords || [], + actions: actions, + primaryAction: { + name: primaryActionLabel, + icon: "open_in_new", + action: "launch" + }, + _hName: "", + _hSub: "", + _hRich: false, + _preScored: undefined + }; +} + +function transformCoreApp(app, openLabel) { + var iconName = "apps"; + var iconType = "material"; + + if (app.icon) { + if (app.icon.startsWith("svg+corner:")) { + iconType = "composite"; + } else if (app.icon.startsWith("material:")) { + iconName = app.icon.substring(9); + } else { + iconName = app.icon; + iconType = "image"; + } + } + + return { + id: app.builtInPluginId || app.action || "", + type: "app", + name: app.name || "", + subtitle: app.comment || "", + icon: iconName, + iconType: iconType, + iconFull: app.icon, + section: "apps", + data: app, + isCore: true, + actions: [], + primaryAction: { + name: openLabel, + icon: "open_in_new", + action: "launch" + }, + _hName: "", + _hSub: "", + _hRich: false, + _preScored: undefined + }; +} + +function transformBuiltInLauncherItem(item, pluginId, openLabel) { + var rawIcon = item.icon || "extension"; + var icon = Utils.stripIconPrefix(rawIcon); + var iconType = item.iconType; + if (!iconType) { + if (rawIcon.startsWith("material:")) + iconType = "material"; + else if (rawIcon.startsWith("unicode:")) + iconType = "unicode"; + else + iconType = "image"; + } + + return { + id: item.action || "", + type: "plugin", + name: item.name || "", + subtitle: item.comment || "", + icon: icon, + iconType: iconType, + section: "plugin_" + pluginId, + data: item, + pluginId: pluginId, + isBuiltInLauncher: true, + keywords: item.keywords || [], + actions: [], + primaryAction: { + name: openLabel, + icon: "open_in_new", + action: "execute" + }, + _hName: "", + _hSub: "", + _hRich: false, + _preScored: item._preScored + }; +} + +function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel, openTerminalLabel) { + var filename = file.path ? file.path.split("/").pop() : ""; + var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : ""; + var isDir = file.is_dir || false; + + var actions = []; + if (isDir) { + if (openTerminalLabel) { + actions.push({ + name: openTerminalLabel, + icon: "terminal", + action: "open_terminal" + }); + } + } else { + actions.push({ + name: openFolderLabel, + icon: "folder_open", + action: "open_folder" + }); + } + actions.push({ + name: copyPathLabel, + icon: "content_copy", + action: "copy_path" + }); + + return { + id: file.path || "", + type: "file", + name: filename, + subtitle: dirname, + icon: isDir ? "folder" : Utils.getFileIcon(filename), + iconType: "material", + section: "files", + data: file, + actions: actions, + primaryAction: { + name: openLabel, + icon: "open_in_new", + action: "open" + }, + _hName: "", + _hSub: "", + _hRich: false, + _preScored: undefined + }; +} + +function transformPluginItem(item, pluginId, selectLabel) { + var rawIcon = item.icon || "extension"; + var icon = Utils.stripIconPrefix(rawIcon); + var iconType = item.iconType; + if (!iconType) { + if (rawIcon.startsWith("material:")) + iconType = "material"; + else if (rawIcon.startsWith("unicode:")) + iconType = "unicode"; + else + iconType = "image"; + } + + return { + id: item.id || item.name || "", + type: "plugin", + name: item.name || "", + subtitle: item.comment || item.description || "", + icon: icon, + iconType: iconType, + section: "plugin_" + pluginId, + data: item, + pluginId: pluginId, + keywords: item.keywords || [], + actions: item.actions || [], + primaryAction: item.primaryAction || { + name: selectLabel, + icon: "check", + action: "execute" + }, + _hName: "", + _hSub: "", + _hRich: false, + _preScored: item._preScored + }; +} + +function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) { + var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension"); + return { + id: "browse_" + pluginId, + type: "plugin_browse", + name: plugin.name || pluginId, + subtitle: trigger ? triggerLabel.replace("%1", trigger) : noTriggerLabel, + icon: isBuiltIn ? rawIcon : Utils.stripIconPrefix(rawIcon), + iconType: isBuiltIn ? "material" : Utils.detectIconType(rawIcon), + section: "browse_plugins", + data: { + pluginId: pluginId, + plugin: plugin, + isBuiltIn: isBuiltIn + }, + actions: [ + { + name: "All", + icon: isAllowed ? "visibility" : "visibility_off", + action: "toggle_all_visibility" + } + ], + primaryAction: { + name: browseLabel, + icon: "arrow_forward", + action: "browse_plugin" + }, + _hName: "", + _hSub: "", + _hRich: false, + _preScored: undefined + }; +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContent.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContent.qml new file mode 100644 index 0000000..4372515 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContent.qml @@ -0,0 +1,1075 @@ +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 + + property bool editMode: false + property var editingApp: null + property string editAppId: "" + + function resetScroll() { + resultsList.resetScroll(); + } + + function focusSearchField() { + searchField.forceActiveFocus(); + } + + function openEditMode(app) { + if (!app) + return; + editingApp = app; + editAppId = app.id || app.execString || app.exec || ""; + var existing = SessionData.getAppOverride(editAppId); + editNameField.text = existing?.name || ""; + editIconField.text = existing?.icon || ""; + editCommentField.text = existing?.comment || ""; + editEnvVarsField.text = existing?.envVars || ""; + editExtraFlagsField.text = existing?.extraFlags || ""; + editDgpuToggle.checked = existing?.launchOnDgpu || false; + editMode = true; + Qt.callLater(() => editNameField.forceActiveFocus()); + } + + function closeEditMode() { + editMode = false; + editingApp = null; + editAppId = ""; + Qt.callLater(() => searchField.forceActiveFocus()); + } + + function saveAppOverride() { + var override = {}; + if (editNameField.text.trim()) + override.name = editNameField.text.trim(); + if (editIconField.text.trim()) + override.icon = editIconField.text.trim(); + if (editCommentField.text.trim()) + override.comment = editCommentField.text.trim(); + if (editEnvVarsField.text.trim()) + override.envVars = editEnvVarsField.text.trim(); + if (editExtraFlagsField.text.trim()) + override.extraFlags = editExtraFlagsField.text.trim(); + if (editDgpuToggle.checked) + override.launchOnDgpu = true; + SessionData.setAppOverride(editAppId, override); + closeEditMode(); + } + + function resetAppOverride() { + SessionData.clearAppOverride(editAppId); + closeEditMode(); + } + + 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?.spotlightOpen ?? 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 + + onEditAppRequested: app => { + root.openEditMode(app); + } + } + + Keys.onPressed: event => { + if (editMode) { + if (event.key === Qt.Key_Escape) { + closeEditMode(); + event.accepted = true; + } + return; + } + + var hasCtrl = event.modifiers & Qt.ControlModifier; + 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: + if (hasCtrl) { + controller.navigateHistory("down"); + } else { + controller.selectNext(); + } + return; + case Qt.Key_Up: + if (hasCtrl) { + controller.navigateHistory("up"); + } else { + controller.selectPrevious(); + } + return; + case Qt.Key_PageDown: + controller.selectPageDown(8); + return; + case Qt.Key_PageUp: + controller.selectPageUp(8); + return; + case Qt.Key_Right: + if (hasCtrl) { + controller.cycleMode(); + return; + } + if (controller.getCurrentSectionViewMode() !== "list") { + controller.selectRight(); + return; + } + event.accepted = false; + return; + case Qt.Key_Left: + if (hasCtrl) { + const reverse = true; + controller.cycleMode(reverse); + return; + } + if (controller.getCurrentSectionViewMode() !== "list") { + controller.selectLeft(); + return; + } + event.accepted = false; + return; + case Qt.Key_H: + if (hasCtrl) { + const reverse = true; + controller.cycleMode(reverse); + 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) { + controller.cycleMode(); + 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 (hasCtrl && actionPanel.hasActions) { + actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show(); + return; + } + controller.selectNext(); + return; + case Qt.Key_Backtab: + if (hasCtrl && actionPanel.expanded) { + const reverse = true; + actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show(); + return; + } + controller.selectPrevious(); + 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) { + controller.setMode("all"); + return; + } + event.accepted = false; + return; + case Qt.Key_2: + if (hasCtrl) { + controller.setMode("apps"); + return; + } + event.accepted = false; + return; + case Qt.Key_3: + if (hasCtrl) { + controller.setMode("files"); + return; + } + event.accepted = false; + return; + case Qt.Key_4: + if (hasCtrl) { + controller.setMode("plugins"); + return; + } + event.accepted = false; + return; + case Qt.Key_Slash: + if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) { + controller.setMode("files", true); + return; + } + event.accepted = false; + return; + default: + event.accepted = false; + } + } + + Item { + anchors.fill: parent + visible: !editMode && !(root.parentModal?.isClosing ?? false) + + Item { + id: footerBar + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: root.parentModal?.borderWidth ?? 1 + anchors.rightMargin: root.parentModal?.borderWidth ?? 1 + anchors.bottomMargin: root.parentModal?.borderWidth ?? 1 + readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter + height: showFooter ? 36 : 0 + visible: showFooter + clip: true + + Rectangle { + anchors.fill: parent + anchors.topMargin: -Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + radius: Theme.cornerRadius + } + + Row { + id: modeButtonsRow + anchors.left: parent.left + anchors.leftMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight + spacing: 2 + + 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 || modeArea.containsMouse ? Theme.primaryContainer : "transparent" + + 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.primary : Theme.surfaceVariantText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.label + font.pixelSize: Theme.fontSizeSmall + color: controller.searchMode === modelData.id ? Theme.primary : 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.spacingS + 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: "Ctrl-Tab " + I18n.tr("actions") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + visible: actionPanel.hasActions + } + } + } + + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: footerBar.top + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingM + spacing: Theme.spacingXS + clip: false + + Row { + width: parent.width + spacing: Theme.spacingS + + 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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + 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 : 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; + } + } + } + } + + Row { + id: categoryRow + width: parent.width + readonly property bool showPluginCategories: controller.activePluginCategories.length > 0 + height: showPluginCategories ? 36 : 0 + visible: showPluginCategories + spacing: Theme.spacingS + + 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 + + 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: 2 + 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 || chipArea.containsMouse ? Theme.primaryContainer : "transparent" + + 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.primary : Theme.surfaceVariantText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.label + font.pixelSize: Theme.fontSizeSmall + color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceText + } + } + + 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 { + width: parent.width + height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) + ResultsList { + id: resultsList + anchors.fill: parent + controller: root.controller + + 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 + selectedItem: controller.selectedItem + controller: controller + } + } + } + + Connections { + target: controller + function onSelectedItemChanged() { + if (actionPanel.expanded && !actionPanel.hasActions) { + actionPanel.hide(); + } + } + function onSearchQueryRequested(query) { + searchField.text = query; + searchField.cursorPosition = query.length; + } + function onModeChanged() { + extFilterField.text = ""; + } + } + + FocusScope { + id: editView + anchors.fill: parent + anchors.margins: Theme.spacingM + visible: editMode + focus: editMode + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + closeEditMode(); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (event.modifiers & Qt.ControlModifier) { + saveAppOverride(); + event.accepted = true; + } + } else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) { + saveAppOverride(); + event.accepted = true; + } + } + + Column { + anchors.fill: parent + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + Rectangle { + width: 40 + height: 40 + radius: Theme.cornerRadius + color: backButtonArea.containsMouse ? Theme.surfaceHover : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "arrow_back" + size: 20 + color: Theme.surfaceText + } + + MouseArea { + id: backButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: closeEditMode() + } + } + + Image { + width: 40 + height: 40 + source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable") + sourceSize.width: 40 + sourceSize.height: 40 + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: I18n.tr("Edit App") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: editingApp?.name || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineMedium + } + + Flickable { + width: parent.width + height: parent.height - y - buttonsRow.height - Theme.spacingM + contentHeight: editFieldsColumn.height + clip: true + boundsBehavior: Flickable.StopAtBounds + + Column { + id: editFieldsColumn + width: parent.width + spacing: Theme.spacingS + + Column { + width: parent.width + spacing: 4 + + StyledText { + text: I18n.tr("Name") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankTextField { + id: editNameField + width: parent.width + placeholderText: editingApp?.name || "" + keyNavigationTab: editIconField + keyNavigationBacktab: editExtraFlagsField + } + } + + Column { + width: parent.width + spacing: 4 + + StyledText { + text: I18n.tr("Icon") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankTextField { + id: editIconField + width: parent.width + placeholderText: editingApp?.icon || "" + keyNavigationTab: editCommentField + keyNavigationBacktab: editNameField + } + } + + Column { + width: parent.width + spacing: 4 + + StyledText { + text: I18n.tr("Description") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankTextField { + id: editCommentField + width: parent.width + placeholderText: editingApp?.comment || "" + keyNavigationTab: editEnvVarsField + keyNavigationBacktab: editIconField + } + } + + Column { + width: parent.width + spacing: 4 + + StyledText { + text: I18n.tr("Environment Variables") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: "KEY=value KEY2=value2" + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankTextField { + id: editEnvVarsField + width: parent.width + placeholderText: "VAR=value" + keyNavigationTab: editExtraFlagsField + keyNavigationBacktab: editCommentField + } + } + + Column { + width: parent.width + spacing: 4 + + StyledText { + text: I18n.tr("Extra Arguments") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankTextField { + id: editExtraFlagsField + width: parent.width + placeholderText: "--flag --option=value" + keyNavigationTab: editNameField + keyNavigationBacktab: editEnvVarsField + } + } + + DankToggle { + id: editDgpuToggle + width: parent.width + text: I18n.tr("Launch on dGPU by default") + visible: SessionService.nvidiaCommand.length > 0 + checked: false + onToggled: checked => editDgpuToggle.checked = checked + } + } + } + + Row { + id: buttonsRow + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Rectangle { + id: resetButton + width: 90 + height: 40 + radius: Theme.cornerRadius + color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha + visible: SessionData.getAppOverride(editAppId) !== null + + StyledText { + text: I18n.tr("Reset") + font.pixelSize: Theme.fontSizeMedium + color: Theme.error + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: resetButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: resetAppOverride() + } + } + + Rectangle { + id: cancelButton + width: 90 + height: 40 + radius: Theme.cornerRadius + color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha + + StyledText { + text: I18n.tr("Cancel") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: cancelButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: closeEditMode() + } + } + + Rectangle { + id: saveButton + width: 90 + height: 40 + radius: Theme.cornerRadius + color: saveButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9) : Theme.primary + + StyledText { + text: I18n.tr("Save") + font.pixelSize: Theme.fontSizeMedium + color: Theme.primaryText + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: saveButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: saveAppOverride() + } + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml new file mode 100644 index 0000000..3c55590 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml @@ -0,0 +1,510 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Popup { + id: root + + property var item: null + property var controller: null + property var searchField: null + property var parentHandler: null + + signal hideRequested + signal editAppRequested(var app) + + 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]; + if (!instance) + return false; + if (typeof instance.getContextMenuActions !== "function") + return false; + var actions = instance.getContextMenuActions(spotlightItem.data); + return Array.isArray(actions) && actions.length > 0; + } + return false; + } + + readonly property bool isCoreApp: item?.type === "app" && !!item?.isCore + readonly property var coreAppData: isCoreApp ? item?.data ?? null : null + readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null + readonly property string appId: { + if (isCoreApp) { + return item?.id || coreAppData?.builtInPluginId || ""; + } + return desktopEntry?.id || desktopEntry?.execString || ""; + } + readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false + readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry + readonly property bool isPluginItem: item?.type === "plugin" + + function getPluginContextMenuActions() { + if (!isPluginItem || !item?.pluginId) + return []; + + var instance = PluginService.pluginInstances[item.pluginId]; + if (!instance) + return []; + if (typeof instance.getContextMenuActions !== "function") + return []; + + var actions = instance.getContextMenuActions(item.data); + if (!Array.isArray(actions)) + return []; + + return actions; + } + + function executePluginAction(actionOrObj) { + var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action; + var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher; + + if (typeof actionFunc === "function") + actionFunc(); + + if (closeLauncher) { + controller?.itemExecuted(); + } else { + controller?.performSearch(); + } + hide(); + } + + readonly property var menuItems: { + var items = []; + + if (isPluginItem) { + var pluginActions = getPluginContextMenuActions(); + for (var i = 0; i < pluginActions.length; i++) { + var act = pluginActions[i]; + items.push({ + type: "item", + icon: act.icon || "play_arrow", + text: act.text || act.name || "", + pluginAction: act + }); + } + return items; + } + + if (item?.type === "app") { + items.push({ + type: "item", + icon: isPinned ? "keep_off" : "push_pin", + text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"), + action: togglePin + }); + } + + if (isRegularApp) { + items.push({ + type: "item", + icon: "visibility_off", + text: I18n.tr("Hide App"), + action: hideCurrentApp + }); + items.push({ + type: "item", + icon: "edit", + text: I18n.tr("Edit App"), + action: editCurrentApp + }); + } + + if (item?.actions && item.actions.length > 0) { + items.push({ + type: "separator" + }); + for (var i = 0; i < item.actions.length; i++) { + var act = item.actions[i]; + items.push({ + type: "item", + icon: act.icon || "play_arrow", + text: act.name || "", + actionData: act + }); + } + } + + items.push({ + type: "separator" + }); + + if (isRegularApp && SessionService.nvidiaCommand) { + items.push({ + type: "item", + icon: "memory", + text: I18n.tr("Launch on dGPU"), + action: launchWithNvidia + }); + } + + items.push({ + type: "item", + icon: "launch", + text: I18n.tr("Launch"), + action: launchApp + }); + + return items; + } + + function show(x, y, spotlightItem, fromKeyboard) { + if (!spotlightItem?.data) + return; + item = spotlightItem; + selectedMenuIndex = fromKeyboard ? 0 : -1; + keyboardNavigation = fromKeyboard; + + 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(); + }); + } + + function hide() { + if (parentHandler) + parentHandler.enabled = true; + close(); + } + + function togglePin() { + if (!appId) + return; + if (isPinned) + SessionData.removePinnedApp(appId); + else + SessionData.addPinnedApp(appId); + hide(); + } + + function hideCurrentApp() { + if (!appId) + return; + SessionData.hideApp(appId); + controller?.performSearch(); + hide(); + } + + function editCurrentApp() { + if (!desktopEntry) + return; + editAppRequested(desktopEntry); + hide(); + } + + function launchApp() { + if (isCoreApp) { + if (!coreAppData) + return; + AppSearchService.executeCoreApp(coreAppData); + controller?.itemExecuted(); + hide(); + return; + } + if (!desktopEntry) + return; + SessionService.launchDesktopEntry(desktopEntry); + AppUsageHistoryData.addAppUsage(desktopEntry); + controller?.itemExecuted(); + hide(); + } + + function launchWithNvidia() { + if (!desktopEntry) + return; + SessionService.launchDesktopEntry(desktopEntry, true); + AppUsageHistoryData.addAppUsage(desktopEntry); + controller?.itemExecuted(); + hide(); + } + + function executeDesktopAction(actionData) { + if (!desktopEntry || !actionData) + return; + SessionService.launchDesktopAction(desktopEntry, actionData.actionData || actionData); + AppUsageHistoryData.addAppUsage(desktopEntry); + controller?.itemExecuted(); + hide(); + } + + property int selectedMenuIndex: 0 + property bool keyboardNavigation: false + + readonly property int visibleItemCount: { + var count = 0; + for (var i = 0; i < menuItems.length; i++) { + if (menuItems[i].type === "item") + count++; + } + return count; + } + + function selectNext() { + if (visibleItemCount > 0) + selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount; + } + + function selectPrevious() { + if (visibleItemCount > 0) + selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount; + } + + function activateSelected() { + var itemIndex = 0; + for (var i = 0; i < menuItems.length; i++) { + if (menuItems[i].type !== "item") + continue; + if (itemIndex === selectedMenuIndex) { + var menuItem = menuItems[i]; + if (menuItem.action) + menuItem.action(); + else if (menuItem.pluginAction) + executePluginAction(menuItem.pluginAction); + else if (menuItem.actionData) + executeDesktopAction(menuItem.actionData); + return; + } + itemIndex++; + } + } + + width: menuContainer.implicitWidth + height: menuContainer.implicitHeight + padding: 0 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + modal: true + dim: false + background: Item {} + + onOpened: { + Qt.callLater(() => keyboardHandler.forceActiveFocus()); + } + + onClosed: { + if (parentHandler) + parentHandler.enabled = true; + if (searchField?.visible) { + Qt.callLater(() => searchField.forceActiveFocus()); + } + } + + 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; + } + } + + Rectangle { + id: menuContainer + anchors.fill: parent + implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2) + implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2 + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + 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 + } + + 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) + } + } + + 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 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "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 + + 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: { + var menuItem = menuItemDelegate.modelData; + if (menuItem.action) + menuItem.action(); + else if (menuItem.pluginAction) + root.executePluginAction(menuItem.pluginAction); + else if (menuItem.actionData) + root.executeDesktopAction(menuItem.actionData); + } + } + } + } + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/NavigationHelpers.js b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/NavigationHelpers.js new file mode 100644 index 0000000..1e78b11 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/NavigationHelpers.js @@ -0,0 +1,254 @@ +.pragma library + +function getFirstItemIndex(flatModel) { + for (var i = 0; i < flatModel.length; i++) { + if (!flatModel[i].isHeader) + return i; + } + return 0; +} + +function findNextNonHeaderIndex(flatModel, startIndex) { + for (var i = startIndex; i < flatModel.length; i++) { + if (!flatModel[i].isHeader) + return i; + } + return -1; +} + +function findPrevNonHeaderIndex(flatModel, startIndex) { + for (var i = startIndex; i >= 0; i--) { + if (!flatModel[i].isHeader) + return i; + } + return -1; +} + +function getSectionBounds(flatModel, sectionId) { + if (flatModel._sectionBounds && flatModel._sectionBounds[sectionId]) + return flatModel._sectionBounds[sectionId]; + + var start = -1, end = -1; + for (var i = 0; i < flatModel.length; i++) { + if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) { + start = i + 1; + } else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) { + end = i; + } else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) { + break; + } + } + return { + start: start, + end: end, + count: end >= start ? end - start + 1 : 0 + }; +} + +function getGridColumns(viewMode, gridColumns) { + switch (viewMode) { + case "tile": + return 3; + case "grid": + return gridColumns; + default: + return 1; + } +} + +function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) { + if (flatModel.length === 0) + return selectedFlatIndex; + + var entry = flatModel[selectedFlatIndex]; + if (!entry || entry.isHeader) { + var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1); + return next !== -1 ? next : selectedFlatIndex; + } + + var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId); + if (actualViewMode === "list") { + var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1); + return next !== -1 ? next : selectedFlatIndex; + } + + var bounds = getSectionBounds(flatModel, entry.sectionId); + var cols = getGridColumns(actualViewMode, gridColumns); + var posInSection = selectedFlatIndex - bounds.start; + var newPosInSection = posInSection + cols; + + if (newPosInSection < bounds.count) { + return bounds.start + newPosInSection; + } + + var currentRow = Math.floor(posInSection / cols); + var lastRow = Math.floor((bounds.count - 1) / cols); + if (currentRow < lastRow) { + return bounds.start + bounds.count - 1; + } + + var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1); + return nextSection !== -1 ? nextSection : selectedFlatIndex; +} + +function calculatePrevIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) { + if (flatModel.length === 0) + return selectedFlatIndex; + + var entry = flatModel[selectedFlatIndex]; + if (!entry || entry.isHeader) { + var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1); + return prev !== -1 ? prev : selectedFlatIndex; + } + + var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId); + if (actualViewMode === "list") { + var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1); + return prev !== -1 ? prev : selectedFlatIndex; + } + + var bounds = getSectionBounds(flatModel, entry.sectionId); + var cols = getGridColumns(actualViewMode, gridColumns); + var posInSection = selectedFlatIndex - bounds.start; + var newPosInSection = posInSection - cols; + + if (newPosInSection >= 0) { + return bounds.start + newPosInSection; + } + + var prevItem = findPrevNonHeaderIndex(flatModel, bounds.start - 1); + return prevItem !== -1 ? prevItem : selectedFlatIndex; +} + +function calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) { + if (flatModel.length === 0) + return selectedFlatIndex; + + var entry = flatModel[selectedFlatIndex]; + if (!entry || entry.isHeader) { + var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1); + return next !== -1 ? next : selectedFlatIndex; + } + + var viewMode = getSectionViewModeFn(entry.sectionId); + if (viewMode === "list") { + var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1); + return next !== -1 ? next : selectedFlatIndex; + } + + var bounds = getSectionBounds(flatModel, entry.sectionId); + var posInSection = selectedFlatIndex - bounds.start; + if (posInSection + 1 < bounds.count) { + return bounds.start + posInSection + 1; + } + return selectedFlatIndex; +} + +function calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) { + if (flatModel.length === 0) + return selectedFlatIndex; + + var entry = flatModel[selectedFlatIndex]; + if (!entry || entry.isHeader) { + var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1); + return prev !== -1 ? prev : selectedFlatIndex; + } + + var viewMode = getSectionViewModeFn(entry.sectionId); + if (viewMode === "list") { + var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1); + return prev !== -1 ? prev : selectedFlatIndex; + } + + var bounds = getSectionBounds(flatModel, entry.sectionId); + var posInSection = selectedFlatIndex - bounds.start; + if (posInSection > 0) { + return bounds.start + posInSection - 1; + } + return selectedFlatIndex; +} + +function calculateNextSectionIndex(flatModel, selectedFlatIndex) { + var currentSection = null; + if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) { + currentSection = flatModel[selectedFlatIndex].sectionId; + } + + var foundCurrent = false; + for (var i = 0; i < flatModel.length; i++) { + if (flatModel[i].isHeader) { + if (foundCurrent) { + for (var j = i + 1; j < flatModel.length; j++) { + if (!flatModel[j].isHeader) + return j; + } + } + if (flatModel[i].section.id === currentSection) { + foundCurrent = true; + } + } + } + return selectedFlatIndex; +} + +function calculatePrevSectionIndex(flatModel, selectedFlatIndex) { + var currentSection = null; + if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) { + currentSection = flatModel[selectedFlatIndex].sectionId; + } + + var lastSectionStart = -1; + var prevSectionStart = -1; + + for (var i = 0; i < flatModel.length; i++) { + if (flatModel[i].isHeader) { + if (flatModel[i].section.id === currentSection) { + break; + } + prevSectionStart = lastSectionStart; + lastSectionStart = i; + } + } + + if (prevSectionStart >= 0) { + for (var j = prevSectionStart + 1; j < flatModel.length; j++) { + if (!flatModel[j].isHeader) + return j; + } + } + return selectedFlatIndex; +} + +function calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems) { + if (flatModel.length === 0) + return selectedFlatIndex; + + var itemsToSkip = visibleItems || 8; + var newIndex = selectedFlatIndex; + + for (var i = 0; i < itemsToSkip; i++) { + var next = findNextNonHeaderIndex(flatModel, newIndex + 1); + if (next === -1) + break; + newIndex = next; + } + + return newIndex; +} + +function calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems) { + if (flatModel.length === 0) + return selectedFlatIndex; + + var itemsToSkip = visibleItems || 8; + var newIndex = selectedFlatIndex; + + for (var i = 0; i < itemsToSkip; i++) { + var prev = findPrevNonHeaderIndex(flatModel, newIndex - 1); + if (prev === -1) + break; + newIndex = prev; + } + + return newIndex; +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultItem.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultItem.qml new file mode 100644 index 0000000..0a50cd3 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultItem.qml @@ -0,0 +1,196 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property var item: null + property bool isSelected: false + property bool isHovered: itemArea.containsMouse || allModeToggleArea.containsMouse + property var controller: null + property int flatIndex: -1 + + signal clicked + signal rightClicked(real mouseX, real mouseY) + + readonly property string iconValue: { + if (!item) + return ""; + switch (item.iconType) { + case "material": + case "nerd": + return "material:" + (item.icon || "apps"); + case "unicode": + return "unicode:" + (item.icon || ""); + case "composite": + return item.iconFull || ""; + case "image": + default: + return item.icon || ""; + } + } + + width: parent?.width ?? 200 + height: 52 + color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent" + radius: Theme.cornerRadius + + DankRipple { + id: rippleLayer + rippleColor: Theme.surfaceText + cornerRadius: root.radius + } + + MouseArea { + id: itemArea + z: 1 + anchors.fill: parent + anchors.rightMargin: root.item?.type === "plugin_browse" ? 40 : 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: mouse => { + if (mouse.button === Qt.LeftButton) + rippleLayer.trigger(mouse.x, mouse.y); + } + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + var scenePos = mapToItem(null, mouse.x, mouse.y); + root.rightClicked(scenePos.x, scenePos.y); + } else { + root.clicked(); + } + } + + onPositionChanged: { + if (root.controller) + root.controller.keyboardNavigationActive = false; + } + } + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + AppIconRenderer { + width: 36 + height: 36 + anchors.verticalCenter: parent.verticalCenter + iconValue: root.iconValue + iconSize: 36 + fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?" + materialIconSizeAdjustment: 12 + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 36 - Theme.spacingM * 3 - rightContent.width + spacing: 2 + + Text { + width: parent.width + text: root.item?._hName ?? root.item?.name ?? "" + textFormat: root.item?._hRich ? Text.RichText : Text.PlainText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + font.family: Theme.fontFamily + color: Theme.surfaceText + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + } + + Text { + width: parent.width + text: root.item?._hSub ?? root.item?.subtitle ?? "" + textFormat: root.item?._hRich ? Text.RichText : Text.PlainText + font.pixelSize: Theme.fontSizeSmall + font.family: Theme.fontFamily + color: Theme.surfaceVariantText + elide: Text.ElideRight + clip: true + visible: (root.item?.subtitle ?? "").length > 0 + horizontalAlignment: Text.AlignLeft + } + } + + Row { + id: rightContent + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Rectangle { + id: allModeToggle + visible: root.item?.type === "plugin_browse" + width: 28 + height: 28 + radius: 14 + anchors.verticalCenter: parent.verticalCenter + color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent" + + property bool isAllowed: { + if (root.item?.type !== "plugin_browse") + return false; + var pluginId = root.item?.data?.pluginId; + if (!pluginId) + return false; + SettingsData.launcherPluginVisibility; + return SettingsData.getPluginAllowWithoutTrigger(pluginId); + } + + DankIcon { + anchors.centerIn: parent + name: allModeToggle.isAllowed ? "visibility" : "visibility_off" + size: 18 + color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText + } + + MouseArea { + id: allModeToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + var pluginId = root.item?.data?.pluginId; + if (!pluginId) + return; + SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed); + } + } + } + + Rectangle { + visible: !!root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse" + width: typeBadge.implicitWidth + Theme.spacingS * 2 + height: 20 + radius: 10 + color: Theme.surfaceVariantAlpha + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: typeBadge + anchors.centerIn: parent + text: { + if (!root.item) + return ""; + switch (root.item.type) { + case "plugin": + return I18n.tr("Plugin"); + case "file": + return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File"); + default: + return ""; + } + } + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.surfaceVariantText + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml new file mode 100644 index 0000000..53a2d45 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml @@ -0,0 +1,498 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property var controller: null + property int gridColumns: controller?.gridColumns ?? 4 + property var _visualRows: [] + property var _flatIndexToRowMap: ({}) + property var _cumulativeHeights: [] + + signal itemRightClicked(int index, var item, real mouseX, real mouseY) + + function _rebuildVisualModel() { + var sections = root.controller?.sections ?? []; + var rows = []; + var indexMap = {}; + var cumHeights = []; + var cumY = 0; + + for (var s = 0; s < sections.length; s++) { + var section = sections[s]; + var sectionId = section.id; + + cumHeights.push(cumY); + rows.push({ + _rowId: "h_" + sectionId, + type: "header", + section: section, + sectionId: sectionId, + height: 32 + }); + cumY += 32; + + if (section.collapsed) + continue; + + var versionTrigger = root.controller?.viewModeVersion ?? 0; + void (versionTrigger); + var mode = root.controller?.getSectionViewMode(sectionId) ?? "list"; + var items = section.items ?? []; + var flatStartIndex = section.flatStartIndex ?? 0; + + if (mode === "list") { + for (var i = 0; i < items.length; i++) { + var flatIdx = flatStartIndex + i; + indexMap[flatIdx] = rows.length; + cumHeights.push(cumY); + rows.push({ + _rowId: items[i].id, + type: "list_item", + item: items[i], + flatIndex: flatIdx, + sectionId: sectionId, + height: 52 + }); + cumY += 52; + } + } else { + var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns; + var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / root.gridColumns); + var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24; + var numRows = Math.ceil(items.length / cols); + + for (var r = 0; r < numRows; r++) { + var rowItems = []; + for (var c = 0; c < cols; c++) { + var idx = r * cols + c; + if (idx >= items.length) + break; + var fi = flatStartIndex + idx; + indexMap[fi] = rows.length; + rowItems.push({ + item: items[idx], + flatIndex: fi + }); + } + cumHeights.push(cumY); + rows.push({ + _rowId: "gr_" + sectionId + "_" + r, + type: "grid_row", + items: rowItems, + sectionId: sectionId, + viewMode: mode, + cols: cols, + height: cellHeight + }); + cumY += cellHeight; + } + } + } + + root._flatIndexToRowMap = indexMap; + root._cumulativeHeights = cumHeights; + root._visualRows = rows; + } + + onGridColumnsChanged: Qt.callLater(_rebuildVisualModel) + onWidthChanged: Qt.callLater(_rebuildVisualModel) + + Connections { + target: root.controller + function onSectionsChanged() { + Qt.callLater(root._rebuildVisualModel); + } + function onViewModeVersionChanged() { + Qt.callLater(root._rebuildVisualModel); + } + function onSearchModeChanged() { + root._visualRows = []; + root._cumulativeHeights = []; + root._flatIndexToRowMap = {}; + } + } + + function resetScroll() { + mainListView.contentY = mainListView.originY; + } + + function ensureVisible(index) { + if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length) + return; + var entry = controller.flatModel[index]; + if (!entry || entry.isHeader) + return; + var rowIndex = _flatIndexToRowMap[index]; + if (rowIndex === undefined) + return; + + mainListView.positionViewAtIndex(rowIndex, ListView.Contain); + + if (stickyHeader.visible && rowIndex < _cumulativeHeights.length) { + var rowY = _cumulativeHeights[rowIndex]; + var scrollY = mainListView.contentY - mainListView.originY; + if (rowY < scrollY + stickyHeader.height) { + mainListView.contentY = Math.max(mainListView.originY, rowY - stickyHeader.height + mainListView.originY); + } + } + } + + function getSelectedItemPosition() { + var fallback = mapToItem(null, width / 2, height / 2); + if (!controller?.flatModel || controller.selectedFlatIndex < 0) + return fallback; + + var entry = controller.flatModel[controller.selectedFlatIndex]; + if (!entry || entry.isHeader) + return fallback; + + var rowIndex = _flatIndexToRowMap[controller.selectedFlatIndex]; + if (rowIndex === undefined) + return fallback; + + var rowY = (rowIndex < _cumulativeHeights.length) ? _cumulativeHeights[rowIndex] : 0; + var row = _visualRows[rowIndex]; + if (!row) + return fallback; + + var itemX = width / 2; + var itemH = row.height; + + if (row.type === "grid_row") { + var rowItems = row.items; + for (var i = 0; i < rowItems.length; i++) { + if (rowItems[i].flatIndex === controller.selectedFlatIndex) { + var cellWidth = row.viewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / row.cols); + itemX = i * cellWidth + cellWidth / 2; + break; + } + } + } + + var visualY = rowY - mainListView.contentY + mainListView.originY + itemH / 2; + var clampedY = Math.max(40, Math.min(height - 40, visualY)); + return mapToItem(null, itemX, clampedY); + } + + Connections { + target: root.controller + function onSelectedFlatIndexChanged() { + if (root.controller?.keyboardNavigationActive) { + Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex)); + } + } + } + + DankListView { + id: mainListView + anchors.fill: parent + clip: true + scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0 + + model: ScriptModel { + values: root._visualRows + objectProp: "_rowId" + } + + add: null + remove: null + displaced: null + move: null + + delegate: Item { + id: delegateRoot + required property var modelData + required property int index + + width: mainListView.width + height: modelData?.height ?? 52 + + SectionHeader { + anchors.fill: parent + visible: delegateRoot.modelData?.type === "header" + section: delegateRoot.modelData?.section ?? null + controller: root.controller + viewMode: { + var vt = root.controller?.viewModeVersion ?? 0; + void (vt); + return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list"; + } + canChangeViewMode: { + var vt = root.controller?.viewModeVersion ?? 0; + void (vt); + return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false; + } + canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false + } + + ResultItem { + anchors.fill: parent + visible: delegateRoot.modelData?.type === "list_item" + item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null + isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex + controller: root.controller + flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1 + + onClicked: { + if (root.controller && delegateRoot.modelData?.item) { + root.controller.executeItem(delegateRoot.modelData.item); + } + } + + onRightClicked: (mouseX, mouseY) => { + root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY); + } + } + + Row { + id: gridRowContent + anchors.fill: parent + visible: delegateRoot.modelData?.type === "grid_row" + + Repeater { + model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : [] + + Item { + id: gridCellDelegate + required property var modelData + required property int index + + readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns)) + + width: cellWidth + height: delegateRoot.height + + GridItem { + width: parent.width - 4 + height: parent.height - 4 + anchors.centerIn: parent + visible: delegateRoot.modelData?.viewMode === "grid" + item: gridCellDelegate.modelData?.item ?? null + isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex + controller: root.controller + flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1 + + onClicked: { + if (root.controller && gridCellDelegate.modelData?.item) { + root.controller.executeItem(gridCellDelegate.modelData.item); + } + } + + onRightClicked: (mouseX, mouseY) => { + root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY); + } + } + + TileItem { + width: parent.width - 4 + height: parent.height - 4 + anchors.centerIn: parent + visible: delegateRoot.modelData?.viewMode === "tile" + item: gridCellDelegate.modelData?.item ?? null + isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex + controller: root.controller + flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1 + + onClicked: { + if (root.controller && gridCellDelegate.modelData?.item) { + root.controller.executeItem(gridCellDelegate.modelData.item); + } + } + + onRightClicked: (mouseX, mouseY) => { + root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY); + } + } + } + } + } + } + } + + Rectangle { + id: bottomShadow + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 24 + z: 100 + visible: { + if (BlurService.enabled) + return false; + if (mainListView.contentHeight <= mainListView.height) + return false; + var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5; + if (atBottom) + return false; + + var flatModel = root.controller?.flatModel; + if (!flatModel || flatModel.length === 0) + return false; + var lastItemIdx = -1; + for (var i = flatModel.length - 1; i >= 0; i--) { + if (!flatModel[i].isHeader) { + lastItemIdx = i; + break; + } + } + if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx) + return false; + return true; + } + gradient: Gradient { + GradientStop { + position: 0.0 + color: "transparent" + } + GradientStop { + position: 1.0 + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + } + } + } + + Rectangle { + id: stickyHeader + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: 32 + z: 101 + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + visible: stickyHeaderSection !== null + + readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0 + + readonly property var stickyHeaderSection: { + var scrollY = mainListView.contentY - mainListView.originY; + if (scrollY <= 0) + return null; + + var rows = root._visualRows; + var heights = root._cumulativeHeights; + if (rows.length === 0 || heights.length === 0) + return null; + + var lo = 0; + var hi = rows.length - 1; + while (lo < hi) { + var mid = (lo + hi + 1) >> 1; + if (mid < heights.length && heights[mid] <= scrollY) + lo = mid; + else + hi = mid - 1; + } + + for (var i = lo; i >= 0; i--) { + if (rows[i].type === "header") + return rows[i].section; + } + return null; + } + + SectionHeader { + width: parent.width + section: stickyHeader.stickyHeaderSection + controller: root.controller + viewMode: { + void (stickyHeader.versionTrigger); + return root.controller?.getSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? "list"; + } + canChangeViewMode: { + void (stickyHeader.versionTrigger); + return root.controller?.canChangeSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? false; + } + canCollapse: { + void (stickyHeader.versionTrigger); + return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false; + } + isSticky: true + } + } + + Item { + anchors.centerIn: parent + visible: (!root.controller?.sections || root.controller.sections.length === 0) && !root.controller?.isFileSearching + width: emptyColumn.implicitWidth + height: emptyColumn.implicitHeight + + Column { + id: emptyColumn + spacing: Theme.spacingM + + DankIcon { + anchors.horizontalCenter: parent.horizontalCenter + name: getEmptyIcon() + size: 48 + color: Theme.outlineButton + + function getEmptyIcon() { + var mode = root.controller?.searchMode ?? "all"; + switch (mode) { + case "files": + var fileType = root.controller?.fileSearchType ?? "all"; + switch (fileType) { + case "dir": + return "folder_open"; + case "file": + return "insert_drive_file"; + default: + return "folder_open"; + } + case "plugins": + return "extension"; + case "apps": + return "apps"; + default: + return "search_off"; + } + } + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: getEmptyText() + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + + function getEmptyText() { + var mode = root.controller?.searchMode ?? "all"; + var hasQuery = root.controller?.searchQuery?.length > 0; + + switch (mode) { + case "files": + if (!DSearchService.dsearchAvailable) + return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch"); + if (!hasQuery) + return I18n.tr("Type to search files"); + if (root.controller.searchQuery.length < 2) + return I18n.tr("Type at least 2 characters"); + var fileType = root.controller?.fileSearchType ?? "all"; + switch (fileType) { + case "dir": + return I18n.tr("No folders found"); + case "file": + return I18n.tr("No files found"); + default: + return I18n.tr("No results found"); + } + case "plugins": + return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins"); + case "apps": + return I18n.tr("No apps found"); + default: + return I18n.tr("No results found"); + } + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Scorer.js b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Scorer.js new file mode 100644 index 0000000..7d54df7 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Scorer.js @@ -0,0 +1,265 @@ +.pragma library + +const Weights = { + exactMatch: 10000, + prefixMatch: 5000, + wordBoundary: 3000, + substring: 500, + fuzzy: 100, + frecency: 2000, + typeBonus: { + app: 1000, + plugin: 900, + file: 800, + action: 600 + } +} + +function tokenize(text) { + return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { return w.length > 0 }) +} + +function hasWordBoundaryMatch(text, query) { + var textWords = tokenize(text) + var queryWords = tokenize(query) + + if (queryWords.length === 0) return false + if (queryWords.length > textWords.length) return false + + for (var i = 0; i <= textWords.length - queryWords.length; i++) { + var allMatch = true + for (var j = 0; j < queryWords.length; j++) { + if (!textWords[i + j].startsWith(queryWords[j])) { + allMatch = false + break + } + } + if (allMatch) return true + } + return false +} + +function levenshteinDistance(s1, s2) { + var len1 = s1.length + var len2 = s2.length + var prev = new Array(len2 + 1) + var curr = new Array(len2 + 1) + + for (var j = 0; j <= len2; j++) + prev[j] = j + + for (var i = 1; i <= len1; i++) { + curr[0] = i + for (var j = 1; j <= len2; j++) { + var cost = s1[i - 1] === s2[j - 1] ? 0 : 1 + curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost) + } + var tmp = prev + prev = curr + curr = tmp + } + return prev[len2] +} + +function fuzzyScore(text, query) { + var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3 + var bestScore = 0 + + if (Math.abs(text.length - query.length) <= maxDistance) { + var distance = levenshteinDistance(text, query) + if (distance <= maxDistance) { + var maxLen = Math.max(text.length, query.length) + bestScore = 1 - (distance / maxLen) + } + } + + var words = tokenize(text) + for (var i = 0; i < words.length && bestScore < 0.8; i++) { + if (Math.abs(words[i].length - query.length) > maxDistance) continue + var wordDistance = levenshteinDistance(words[i], query) + if (wordDistance <= maxDistance) { + var wordMaxLen = Math.max(words[i].length, query.length) + var score = 1 - (wordDistance / wordMaxLen) + bestScore = Math.max(bestScore, score) + } + } + + return bestScore +} + +function getTimeBucketWeight(daysSinceUsed) { + for (var i = 0; i < TimeBuckets.length; i++) { + if (daysSinceUsed <= TimeBuckets[i].maxDays) { + return TimeBuckets[i].weight + } + } + return 10 +} + +function calculateTextScore(name, query) { + if (name === query) return Weights.exactMatch + if (name.startsWith(query)) return Weights.prefixMatch + if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary + if (name.includes(query)) return Weights.substring + + if (query.length >= 3) { + var fs = fuzzyScore(name, query) + if (fs > 0) return fs * Weights.fuzzy + } + + return 0 +} + +function score(item, query, frecencyData) { + var typeBonus = Weights.typeBonus[item.type] || 0 + + if (!query || query.length === 0) { + var usageCount = frecencyData ? frecencyData.usageCount : 0 + return typeBonus + (usageCount * 100) + } + + var name = (item.name || "").toLowerCase() + var q = query.toLowerCase() + + var textScore = calculateTextScore(name, q) + + if (textScore === 0 && item.subtitle) { + var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q) + textScore = subtitleScore * 0.5 + } + + if (textScore === 0 && item.keywords) { + for (var i = 0; i < item.keywords.length; i++) { + var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q) + if (keywordScore > 0) { + textScore = keywordScore * 0.3 + break + } + } + } + + if (textScore === 0) return 0 + + var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0 + + return textScore + usageBonus + typeBonus +} + +function scoreItems(items, query, getFrecencyFn) { + var scored = [] + + for (var i = 0; i < items.length; i++) { + var item = items[i] + var itemScore + + if (item._preScored !== undefined && (query || item._preScored > 900)) { + itemScore = item._preScored + } else { + var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null + itemScore = score(item, query, frecencyData) + } + + if (itemScore > 0 || !query || query.length === 0) { + scored.push({ + item: item, + score: itemScore + }) + } + } + + scored.sort(function (a, b) { + return b.score - a.score + }) + + return scored +} + +function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSection) { + var sections = {} + var result = [] + var limit = maxPerSection || 50 + + for (var i = 0; i < sectionOrder.length; i++) { + var sectionId = sectionOrder[i].id + sections[sectionId] = { + id: sectionId, + title: sectionOrder[i].title, + icon: sectionOrder[i].icon, + priority: sectionOrder[i].priority, + items: [], + collapsed: false, + flatStartIndex: 0 + } + } + + for (var i = 0; i < scoredItems.length; i++) { + var scoredItem = scoredItems[i] + var item = scoredItem.item + var sectionId = item.section || "apps" + + if (sections[sectionId] && sections[sectionId].items.length < limit) { + sections[sectionId].items.push(item) + } else if (sections["apps"] && sections["apps"].items.length < limit) { + sections["apps"].items.push(item) + } + } + + for (var i = 0; i < sectionOrder.length; i++) { + var section = sections[sectionOrder[i].id] + if (section && section.items.length > 0) { + if (sortAlphabetically && section.id === "apps") { + section.items.sort(function (a, b) { + return (a.name || "").localeCompare(b.name || "") + }) + } + result.push(section) + } + } + + return result +} + +function flattenSections(sections) { + var flat = [] + flat._sectionBounds = null + var bounds = {} + + for (var i = 0; i < sections.length; i++) { + var section = sections[i] + + flat.push({ + isHeader: true, + section: section, + sectionId: section.id, + sectionIndex: i + }) + + var itemStart = flat.length + section.flatStartIndex = itemStart + + if (!section.collapsed) { + for (var j = 0; j < section.items.length; j++) { + flat.push({ + isHeader: false, + item: section.items[j], + sectionId: section.id, + sectionIndex: i, + indexInSection: j + }) + } + } + + var itemEnd = flat.length - 1 + var itemCount = flat.length - itemStart + if (itemCount > 0) { + bounds[section.id] = { + start: itemStart, + end: itemEnd, + count: itemCount + } + } + } + + flat._sectionBounds = bounds + return flat +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Section.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Section.qml new file mode 100644 index 0000000..aba4c2f --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Section.qml @@ -0,0 +1,114 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Common + +Item { + id: root + + property var section: null + property var controller: null + property string viewMode: "list" + property int gridColumns: 4 + property int startIndex: 0 + + signal itemClicked(int flatIndex) + signal itemRightClicked(int flatIndex, var item, real mouseX, real mouseY) + + height: headerItem.height + (section?.collapsed ? 0 : contentLoader.height + Theme.spacingXS) + width: parent?.width ?? 200 + + SectionHeader { + id: headerItem + width: parent.width + section: root.section + controller: root.controller + viewMode: root.viewMode + canChangeViewMode: root.controller?.canChangeSectionViewMode(root.section?.id) ?? true + + onViewModeToggled: { + if (root.controller && root.section) { + var newMode = root.viewMode === "list" ? "grid" : "list"; + root.controller.setSectionViewMode(root.section.id, newMode); + } + } + } + + Loader { + id: contentLoader + anchors.top: headerItem.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Theme.spacingXS + active: !root.section?.collapsed + visible: active + + sourceComponent: root.viewMode === "grid" ? gridComponent : listComponent + + Component { + id: listComponent + + Column { + spacing: 2 + width: contentLoader.width + + Repeater { + model: ScriptModel { + values: root.section?.items ?? [] + objectProp: "id" + } + + ResultItem { + required property var modelData + required property int index + + width: parent?.width ?? 200 + item: modelData + isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex + controller: root.controller + flatIndex: root.startIndex + index + + onClicked: root.itemClicked(root.startIndex + index) + onRightClicked: (mouseX, mouseY) => { + root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY); + } + } + } + } + } + + Component { + id: gridComponent + + Flow { + width: contentLoader.width + spacing: 4 + + Repeater { + model: ScriptModel { + values: root.section?.items ?? [] + objectProp: "id" + } + + GridItem { + required property var modelData + required property int index + + width: Math.floor(contentLoader.width / root.gridColumns) + height: width + 24 + item: modelData + isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex + controller: root.controller + flatIndex: root.startIndex + index + + onClicked: root.itemClicked(root.startIndex + index) + onRightClicked: (mouseX, mouseY) => { + root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY); + } + } + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/SectionHeader.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/SectionHeader.qml new file mode 100644 index 0000000..ee366c7 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/SectionHeader.qml @@ -0,0 +1,340 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var section: null + property var controller: null + property string viewMode: "list" + property bool canChangeViewMode: true + property bool canCollapse: true + property bool isSticky: false + + signal viewModeToggled + + width: parent?.width ?? 200 + height: 32 + color: isSticky ? "transparent" : (hoverArea.containsMouse ? Theme.surfaceHover : "transparent") + radius: Theme.cornerRadius + + MouseArea { + id: hoverArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + + Row { + id: leftContent + anchors.left: parent.left + anchors.leftMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + // Whether the apps category picker should replace the plain title + readonly property bool hasAppCategories: root.section?.id === "apps" && (root.controller?.appCategories?.length ?? 0) > 0 + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + // Hide section icon when the category chip already shows one + visible: !leftContent.hasAppCategories + name: root.section?.icon ?? "folder" + size: 16 + color: Theme.surfaceVariantText + } + + // Plain title — hidden when the category chip is shown + StyledText { + anchors.verticalCenter: parent.verticalCenter + visible: !leftContent.hasAppCategories + text: root.section?.title ?? "" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceVariantText + } + + // Compact inline category chip — only visible on the apps section + Item { + id: categoryChip + visible: leftContent.hasAppCategories + anchors.verticalCenter: parent.verticalCenter + // Size to content with a fixed-min width so it doesn't jump around + width: chipRow.implicitWidth + Theme.spacingM * 2 + height: 24 + + readonly property string currentCategory: root.controller?.appCategory || (root.controller?.appCategories?.length > 0 ? root.controller.appCategories[0] : "") + readonly property var iconMap: { + const cats = root.controller?.appCategories ?? []; + const m = {}; + cats.forEach(c => { m[c] = AppSearchService.getCategoryIcon(c); }); + return m; + } + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: chipArea.containsMouse || categoryPopup.visible ? Theme.surfaceContainerHigh : "transparent" + border.color: categoryPopup.visible ? Theme.primary : Theme.outlineMedium + border.width: categoryPopup.visible ? 2 : 1 + } + + Row { + id: chipRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: categoryChip.iconMap[categoryChip.currentCategory] ?? "apps" + size: 14 + color: Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: categoryChip.currentCategory + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: categoryPopup.visible ? "expand_less" : "expand_more" + size: 14 + color: Theme.surfaceVariantText + } + } + + MouseArea { + id: chipArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (categoryPopup.visible) { + categoryPopup.close(); + } else { + const pos = categoryChip.mapToItem(Overlay.overlay, 0, 0); + categoryPopup.x = pos.x; + categoryPopup.y = pos.y + categoryChip.height + 4; + categoryPopup.open(); + } + } + } + + Popup { + id: categoryPopup + parent: Overlay.overlay + width: Math.max(categoryChip.width, 180) + padding: 0 + modal: true + dim: false + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { color: "transparent" } + + contentItem: Rectangle { + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1) + border.color: Theme.primary + border.width: 2 + + ElevationShadow { + anchors.fill: parent + z: -1 + level: Theme.elevationLevel2 + fallbackOffset: 4 + targetRadius: parent.radius + targetColor: parent.color + borderColor: parent.border.color + borderWidth: parent.border.width + shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled + } + + ListView { + id: categoryList + anchors.fill: parent + anchors.margins: Theme.spacingS + model: root.controller?.appCategories ?? [] + spacing: 2 + clip: true + interactive: contentHeight > height + implicitHeight: contentHeight + + delegate: Rectangle { + id: catDelegate + required property string modelData + required property int index + width: categoryList.width + height: 32 + radius: Theme.cornerRadius + readonly property bool isCurrent: categoryChip.currentCategory === modelData + color: isCurrent ? Theme.primaryHover : catArea.containsMouse ? Theme.primaryHoverLight : "transparent" + + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: categoryChip.iconMap[catDelegate.modelData] ?? "apps" + size: 16 + color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: catDelegate.modelData + font.pixelSize: Theme.fontSizeMedium + color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText + font.weight: catDelegate.isCurrent ? Font.Medium : Font.Normal + } + } + + MouseArea { + id: catArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.controller) + root.controller.setAppCategory(catDelegate.modelData); + categoryPopup.close(); + } + } + } + } + } + + // Size to list content, cap at 10 visible items + height: Math.min((root.controller?.appCategories?.length ?? 0) * 34, 10 * 34) + Theme.spacingS * 2 + 4 + } + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: root.section?.items?.length ?? 0 + font.pixelSize: Theme.fontSizeSmall + color: Theme.outlineButton + } + } + + Row { + id: rightContent + anchors.right: parent.right + anchors.rightMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Row { + id: viewModeRow + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + visible: root.canChangeViewMode && !root.section?.collapsed + + Repeater { + model: [ + { + mode: "list", + icon: "view_list" + }, + { + mode: "grid", + icon: "grid_view" + }, + { + mode: "tile", + icon: "view_module" + } + ] + + Rectangle { + required property var modelData + required property int index + + width: 20 + height: 20 + radius: 4 + color: root.viewMode === modelData.mode ? Theme.primaryHover : modeArea.containsMouse ? Theme.surfaceHover : "transparent" + + DankIcon { + anchors.centerIn: parent + name: parent.modelData.icon + size: 14 + color: root.viewMode === parent.modelData.mode ? Theme.primary : Theme.surfaceVariantText + } + + MouseArea { + id: modeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.viewMode !== parent.modelData.mode && root.controller && root.section) { + root.controller.setSectionViewMode(root.section.id, parent.modelData.mode); + } + } + } + } + } + } + + Item { + id: collapseButton + width: root.canCollapse ? 24 : 0 + height: 24 + visible: root.canCollapse + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: root.section?.collapsed ? "expand_more" : "expand_less" + size: 16 + color: collapseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText + } + + MouseArea { + id: collapseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.controller && root.section) { + root.controller.toggleSection(root.section.id); + } + } + } + } + } + + MouseArea { + anchors.fill: parent + anchors.rightMargin: rightContent.width + Theme.spacingS + cursorShape: root.canCollapse ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: root.canCollapse + onClicked: { + if (root.canCollapse && root.controller && root.section) { + root.controller.toggleSection(root.section.id); + } + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 1 + color: Theme.outlineMedium + visible: root.isSticky + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/TileItem.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/TileItem.qml new file mode 100644 index 0000000..0b48802 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/TileItem.qml @@ -0,0 +1,199 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell.Wayland +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var item: null + property bool isSelected: false + property bool isHovered: itemArea.containsMouse + property var controller: null + property int flatIndex: -1 + + signal clicked + signal rightClicked(real mouseX, real mouseY) + + radius: Theme.cornerRadius + color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent" + border.width: isSelected ? 2 : 0 + border.color: Theme.primary + + readonly property string toplevelId: item?.data?.toplevelId ?? "" + readonly property var waylandToplevel: { + if (!toplevelId || !item?.pluginId) + return null; + const pluginInstance = PluginService.pluginInstances[item.pluginId]; + if (!pluginInstance?.getToplevelById) + return null; + return pluginInstance.getToplevelById(toplevelId); + } + readonly property bool hasScreencopy: waylandToplevel !== null + + readonly property string iconValue: { + if (!item) + return ""; + if (hasScreencopy) + return ""; + var data = item.data; + if (data?.imageUrl) + return "image:" + data.imageUrl; + if (data?.imagePath) + return "image:" + data.imagePath; + if (data?.path && isImageFile(data.path)) + return "image:" + data.path; + switch (item.iconType) { + case "material": + case "nerd": + return "material:" + (item.icon || "image"); + case "unicode": + return "unicode:" + (item.icon || ""); + case "composite": + return item.iconFull || ""; + case "image": + default: + return item.icon || ""; + } + } + + function isImageFile(path) { + if (!path) + return false; + var ext = path.split('.').pop().toLowerCase(); + return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "jxl", "avif", "heif", "exr"].indexOf(ext) >= 0; + } + + DankRipple { + id: rippleLayer + rippleColor: Theme.surfaceText + cornerRadius: root.radius + } + + Item { + anchors.fill: parent + anchors.margins: 4 + + Rectangle { + id: imageContainer + anchors.fill: parent + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + clip: true + + ScreencopyView { + id: screencopyView + anchors.fill: parent + captureSource: root.waylandToplevel + live: root.hasScreencopy + visible: root.hasScreencopy + + Rectangle { + anchors.fill: parent + color: root.isHovered ? Theme.withAlpha(Theme.surfaceVariant, 0.2) : "transparent" + } + } + + AppIconRenderer { + anchors.fill: parent + iconValue: root.iconValue + iconSize: Math.min(parent.width, parent.height) + fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?" + materialIconSizeAdjustment: iconSize * 0.3 + visible: !root.hasScreencopy + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: labelText.implicitHeight + Theme.spacingS * 2 + color: Theme.withAlpha(Theme.surfaceContainer, 0.85) + visible: root.item?.name?.length > 0 + + Text { + id: labelText + anchors.fill: parent + anchors.margins: Theme.spacingXS + text: root.item?._hName ?? root.item?.name ?? "" + textFormat: root.item?._hRich ? Text.RichText : Text.PlainText + font.pixelSize: Theme.fontSizeSmall + font.family: Theme.fontFamily + color: Theme.surfaceText + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + } + + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingXS + width: 20 + height: 20 + radius: 10 + color: Theme.primary + visible: root.isSelected + + DankIcon { + anchors.centerIn: parent + name: "check" + size: 14 + color: Theme.primaryText + } + } + + Rectangle { + id: attributionBadge + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Theme.spacingXS + width: root.hasScreencopy ? 28 : 40 + height: root.hasScreencopy ? 28 : 16 + radius: root.hasScreencopy ? 14 : 4 + color: root.hasScreencopy ? Theme.surfaceContainer : "transparent" + visible: attributionImage.status === Image.Ready + opacity: 0.95 + + Image { + id: attributionImage + anchors.fill: parent + anchors.margins: root.hasScreencopy ? 4 : 0 + fillMode: Image.PreserveAspectFit + source: root.item?.data?.attribution || "" + mipmap: true + } + } + } + } + + MouseArea { + id: itemArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: mouse => { + if (mouse.button === Qt.LeftButton) + rippleLayer.trigger(mouse.x, mouse.y); + } + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + var scenePos = mapToItem(null, mouse.x, mouse.y); + root.rightClicked(scenePos.x, scenePos.y); + return; + } + root.clicked(); + } + + onPositionChanged: { + if (root.controller) + root.controller.keyboardNavigationActive = false; + } + } +} -- cgit v1.3