diff --git a/Modals/DankModal.qml b/Modals/DankModal.qml index 6c1091ee..00277ee3 100644 --- a/Modals/DankModal.qml +++ b/Modals/DankModal.qml @@ -8,6 +8,7 @@ PanelWindow { id: root property alias content: contentLoader.sourceComponent + property alias contentLoader: contentLoader property real width: 400 property real height: 300 readonly property real screenWidth: screen ? screen.width : 1920 @@ -201,8 +202,8 @@ PanelWindow { onVisibleChanged: { if (visible) Qt.callLater(function() { - focusScope.forceActiveFocus(); - }); + focusScope.forceActiveFocus(); + }); } } diff --git a/Modals/SpotlightModal.qml b/Modals/SpotlightModal.qml index 92103398..b604a383 100644 --- a/Modals/SpotlightModal.qml +++ b/Modals/SpotlightModal.qml @@ -10,842 +10,818 @@ import qs.Services import qs.Widgets DankModal { - id: spotlightModal + id: spotlightModal - property bool spotlightOpen: false + property bool spotlightOpen: false + property Component spotlightContent - function show() { + spotlightContent: Component { + Item { + id: spotlightKeyHandler - spotlightOpen = true + property alias appLauncher: appLauncher + property alias searchField: searchField - appLauncher.searchQuery = "" - } + anchors.fill: parent + focus: true + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + hide(); + event.accepted = true; + } else if (event.key === Qt.Key_Down) { + appLauncher.selectNext(); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + appLauncher.selectPrevious(); + event.accepted = true; + } else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") { + appLauncher.selectNextInRow(); + event.accepted = true; + } else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") { + appLauncher.selectPreviousInRow(); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + appLauncher.launchSelected(); + event.accepted = true; + } else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) { + searchField.forceActiveFocus(); + searchField.insertText(event.text); + event.accepted = true; + } + } - function hide() { - spotlightOpen = false - appLauncher.searchQuery = "" - appLauncher.selectedIndex = 0 - appLauncher.setCategory("All") - } + AppLauncher { + id: appLauncher - function toggle() { - if (spotlightOpen) - hide() - else - show() - } + viewMode: SettingsData.spotlightModalViewMode + gridColumns: 4 + onAppLaunched: hide() + onViewModeSelected: function(mode) { + SettingsData.setSpotlightModalViewMode(mode); + } + } - visible: spotlightOpen - width: 550 - height: 600 - keyboardFocus: "ondemand" - backgroundColor: Theme.popupBackground() - cornerRadius: Theme.cornerRadius - borderColor: Theme.outlineMedium - borderWidth: 1 - enableShadow: true - onVisibleChanged: { + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL - if (visible && !spotlightOpen) - show() - } - onBackgroundClicked: { - spotlightOpen = false - } - Component.onCompleted: { + Rectangle { + width: parent.width + height: categorySelector.height + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceVariantAlpha + border.color: Theme.outlineMedium + border.width: 1 + visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0 - } + CategorySelector { + id: categorySelector - AppLauncher { - id: appLauncher + anchors.centerIn: parent + width: parent.width - Theme.spacingM * 2 + categories: appLauncher.categories + selectedCategory: appLauncher.selectedCategory + compact: false + onCategorySelected: (category) => { + return appLauncher.setCategory(category); + } + } - viewMode: SettingsData.spotlightModalViewMode - gridColumns: 4 - onAppLaunched: hide() - onViewModeSelected: function (mode) { - SettingsData.setSpotlightModalViewMode(mode) - } - } + } - IpcHandler { - function open() { + Row { + width: parent.width + spacing: Theme.spacingM + + DankTextField { + id: searchField + + width: parent.width - 80 - Theme.spacingM // Leave space for view toggle buttons + height: 56 + cornerRadius: Theme.cornerRadius + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7) + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + leftIconName: "search" + leftIconSize: Theme.iconSize + leftIconColor: Theme.surfaceVariantText + leftIconFocusedColor: Theme.primary + showClearButton: true + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge + enabled: spotlightOpen + placeholderText: "Search applications..." + ignoreLeftRightKeys: true + keyForwardTargets: [spotlightKeyHandler] + text: appLauncher.searchQuery + onTextEdited: { + appLauncher.searchQuery = text; + } + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + hide(); + event.accepted = true; + } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) { + if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) + appLauncher.launchSelected(); + else if (appLauncher.model.count > 0) + appLauncher.launchApp(appLauncher.model.get(0)); + event.accepted = true; + } else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) { + event.accepted = false; + } + } + + + } + + Row { + spacing: Theme.spacingXS + visible: appLauncher.model.count > 0 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: 36 + height: 36 + radius: Theme.cornerRadius + color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent" + border.color: appLauncher.viewMode === "list" ? Theme.primarySelected : "transparent" + border.width: 1 + + DankIcon { + anchors.centerIn: parent + name: "view_list" + size: 18 + color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText + } + + MouseArea { + id: listViewArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + appLauncher.setViewMode("list"); + } + } + + } + + Rectangle { + width: 36 + height: 36 + radius: Theme.cornerRadius + color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent" + border.color: appLauncher.viewMode === "grid" ? Theme.primarySelected : "transparent" + border.width: 1 + + DankIcon { + anchors.centerIn: parent + name: "grid_view" + size: 18 + color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText + } + + MouseArea { + id: gridViewArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + appLauncher.setViewMode("grid"); + } + } + + } + + } + + } + + Rectangle { + id: resultsContainer + + width: parent.width + height: parent.height - y + radius: Theme.cornerRadius + color: Theme.surfaceLight + border.color: Theme.outlineLight + border.width: 1 + + DankListView { + id: resultsList + + property int itemHeight: 60 + property int iconSize: 40 + property bool showDescription: true + property int itemSpacing: Theme.spacingS + property bool hoverUpdatesSelection: false + property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive + + signal keyboardNavigationReset() + signal itemClicked(int index, var modelData) + signal itemHovered(int index) + signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) + + function ensureVisible(index) { + if (index < 0 || index >= count) + return ; + + var itemY = index * (itemHeight + itemSpacing); + var itemBottom = itemY + itemHeight; + if (itemY < contentY) + contentY = itemY; + else if (itemBottom > contentY + height) + contentY = itemBottom - height; + } + + anchors.fill: parent + anchors.margins: Theme.spacingS + visible: appLauncher.viewMode === "list" + model: appLauncher.model + currentIndex: appLauncher.selectedIndex + clip: true + spacing: itemSpacing + focus: true + interactive: true + cacheBuffer: Math.max(0, Math.min(height * 2, 1000)) + reuseItems: true + onCurrentIndexChanged: { + if (keyboardNavigationActive) + ensureVisible(currentIndex); + + } + onItemClicked: function(index, modelData) { + appLauncher.launchApp(modelData); + } + onItemHovered: function(index) { + appLauncher.selectedIndex = index; + } + onItemRightClicked: function(index, modelData, mouseX, mouseY) { + contextMenu.show(mouseX, mouseY, modelData); + } + onKeyboardNavigationReset: { + appLauncher.keyboardNavigationActive = false; + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + } + + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AlwaysOff + } + + delegate: Rectangle { + width: ListView.view.width + height: resultsList.itemHeight + radius: Theme.cornerRadius + color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) + border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium + border.width: ListView.isCurrentItem ? 2 : 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingL + + Item { + width: resultsList.iconSize + height: resultsList.iconSize + anchors.verticalCenter: parent.verticalCenter + + IconImage { + id: listIconImg + + anchors.fill: parent + source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : "" + smooth: true + asynchronous: true + visible: status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !listIconImg.visible + color: Theme.surfaceLight + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.primarySelected + + StyledText { + anchors.centerIn: parent + text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" + font.pixelSize: resultsList.iconSize * 0.4 + color: Theme.primary + font.weight: Font.Bold + } + + } + + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - resultsList.iconSize - Theme.spacingL + spacing: Theme.spacingXS + + StyledText { + width: parent.width + text: model.name || "" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + } + + StyledText { + width: parent.width + text: model.comment || "Application" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + elide: Text.ElideRight + visible: resultsList.showDescription && model.comment && model.comment.length > 0 + } + + } + + } + + MouseArea { + id: listMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + z: 10 + onEntered: { + if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive) + resultsList.currentIndex = index; + + resultsList.itemHovered(index); + } + onPositionChanged: { + resultsList.keyboardNavigationReset(); + } + onClicked: (mouse) => { + if (mouse.button === Qt.LeftButton) { + resultsList.itemClicked(index, model); + } else if (mouse.button === Qt.RightButton) { + var globalPos = mapToGlobal(mouse.x, mouse.y); + resultsList.itemRightClicked(index, model, globalPos.x, globalPos.y); + } + } + } + + } + + } + + DankGridView { + id: resultsGrid + + property int currentIndex: appLauncher.selectedIndex + property int columns: 4 + property bool adaptiveColumns: false + property int minCellWidth: 120 + property int maxCellWidth: 160 + property int cellPadding: 8 + property real iconSizeRatio: 0.55 + property int maxIconSize: 48 + property int minIconSize: 32 + property bool hoverUpdatesSelection: false + property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive + property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns + property int baseCellHeight: baseCellWidth + 20 + property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns + property int remainingSpace: width - (actualColumns * cellWidth) + + signal keyboardNavigationReset() + signal itemClicked(int index, var modelData) + signal itemHovered(int index) + signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) + + function ensureVisible(index) { + if (index < 0 || index >= count) + return ; + + var itemY = Math.floor(index / actualColumns) * cellHeight; + var itemBottom = itemY + cellHeight; + if (itemY < contentY) + contentY = itemY; + else if (itemBottom > contentY + height) + contentY = itemBottom - height; + } + + anchors.fill: parent + anchors.margins: Theme.spacingS + visible: appLauncher.viewMode === "grid" + model: appLauncher.model + clip: true + cellWidth: baseCellWidth + cellHeight: baseCellHeight + leftMargin: Math.max(Theme.spacingS, remainingSpace / 2) + rightMargin: leftMargin + focus: true + interactive: true + cacheBuffer: Math.max(0, Math.min(height * 2, 1000)) + reuseItems: true + onCurrentIndexChanged: { + if (keyboardNavigationActive) + ensureVisible(currentIndex); + + } + onItemClicked: function(index, modelData) { + appLauncher.launchApp(modelData); + } + onItemHovered: function(index) { + appLauncher.selectedIndex = index; + } + onItemRightClicked: function(index, modelData, mouseX, mouseY) { + contextMenu.show(mouseX, mouseY, modelData); + } + onKeyboardNavigationReset: { + appLauncher.keyboardNavigationActive = false; + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AlwaysOff + } + + delegate: Rectangle { + width: resultsGrid.cellWidth - resultsGrid.cellPadding + height: resultsGrid.cellHeight - resultsGrid.cellPadding + radius: Theme.cornerRadius + color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) + border.color: resultsGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium + border.width: resultsGrid.currentIndex === index ? 2 : 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + Item { + property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio)) + + width: iconSize + height: iconSize + anchors.horizontalCenter: parent.horizontalCenter + + IconImage { + id: gridIconImg + + anchors.fill: parent + source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : "" + smooth: true + asynchronous: true + visible: status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !gridIconImg.visible + color: Theme.surfaceLight + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.primarySelected + + StyledText { + anchors.centerIn: parent + text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" + font.pixelSize: Math.min(28, parent.width * 0.5) + color: Theme.primary + font.weight: Font.Bold + } + + } + + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + width: resultsGrid.cellWidth - 12 + text: model.name || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 + wrapMode: Text.WordWrap + } + + } + + MouseArea { + id: gridMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + z: 10 + onEntered: { + if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive) + resultsGrid.currentIndex = index; + + resultsGrid.itemHovered(index); + } + onPositionChanged: { + resultsGrid.keyboardNavigationReset(); + } + onClicked: (mouse) => { + if (mouse.button === Qt.LeftButton) { + resultsGrid.itemClicked(index, model); + } else if (mouse.button === Qt.RightButton) { + var globalPos = mapToGlobal(mouse.x, mouse.y); + resultsGrid.itemRightClicked(index, model, globalPos.x, globalPos.y); + } + } + } + + } + + } + + } + + } + + } - spotlightModal.show() - return "SPOTLIGHT_OPEN_SUCCESS" } - function close() { + function show() { + spotlightOpen = true; + if (contentLoader.item && contentLoader.item.appLauncher) + contentLoader.item.appLauncher.searchQuery = ""; - spotlightModal.hide() - return "SPOTLIGHT_CLOSE_SUCCESS" + } + + + function hide() { + spotlightOpen = false; + if (contentLoader.item && contentLoader.item.appLauncher) { + contentLoader.item.appLauncher.searchQuery = ""; + contentLoader.item.appLauncher.selectedIndex = 0; + contentLoader.item.appLauncher.setCategory("All"); + } } function toggle() { - - spotlightModal.toggle() - return "SPOTLIGHT_TOGGLE_SUCCESS" + if (spotlightOpen) + hide(); + else + show(); } - target: "spotlight" - } - - property Component spotlightContent: Component { - Item { - id: spotlightKeyHandler - - anchors.fill: parent - focus: true - Keys.onPressed: function (event) { - if (event.key === Qt.Key_Escape) { - hide() - event.accepted = true - } else if (event.key === Qt.Key_Down) { - appLauncher.selectNext() - event.accepted = true - } else if (event.key === Qt.Key_Up) { - appLauncher.selectPrevious() - event.accepted = true - } else if (event.key === Qt.Key_Right - && appLauncher.viewMode === "grid") { - appLauncher.selectNextInRow() - event.accepted = true - } else if (event.key === Qt.Key_Left - && appLauncher.viewMode === "grid") { - appLauncher.selectPreviousInRow() - event.accepted = true - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - appLauncher.launchSelected() - event.accepted = true - } else if (!searchField.activeFocus && event.text - && event.text.length > 0 && event.text.match( - /[a-zA-Z0-9\\s]/)) { - searchField.forceActiveFocus() - searchField.insertText(event.text) - event.accepted = true + visible: spotlightOpen + width: 550 + height: 600 + keyboardFocus: "ondemand" + backgroundColor: Theme.popupBackground() + cornerRadius: Theme.cornerRadius + borderColor: Theme.outlineMedium + borderWidth: 1 + enableShadow: true + onVisibleChanged: { + if (visible && !spotlightOpen) + show(); + + if (visible && contentLoader.item) { + Qt.callLater(function() { + if (contentLoader.item.searchField) { + contentLoader.item.searchField.forceActiveFocus(); + } + }); } - } - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingL - - Rectangle { - width: parent.width - height: categorySelector.height + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Theme.surfaceVariantAlpha - border.color: Theme.outlineMedium - border.width: 1 - visible: appLauncher.categories.length > 1 - || appLauncher.model.count > 0 - - CategorySelector { - id: categorySelector - - anchors.centerIn: parent - width: parent.width - Theme.spacingM * 2 - categories: appLauncher.categories - selectedCategory: appLauncher.selectedCategory - compact: false - onCategorySelected: category => { - return appLauncher.setCategory(category) - } - } + } + onBackgroundClicked: { + spotlightOpen = false; + } + Component.onCompleted: { + } + content: spotlightContent + IpcHandler { + function open() { + spotlightModal.show(); + return "SPOTLIGHT_OPEN_SUCCESS"; } - Row { - width: parent.width - spacing: Theme.spacingM + function close() { + spotlightModal.hide(); + return "SPOTLIGHT_CLOSE_SUCCESS"; + } - DankTextField { - id: searchField + function toggle() { + spotlightModal.toggle(); + return "SPOTLIGHT_TOGGLE_SUCCESS"; + } - width: parent.width - 80 - Theme.spacingM // Leave space for view toggle buttons - height: 56 - cornerRadius: Theme.cornerRadius - backgroundColor: Qt.rgba(Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, - Theme.getContentBackgroundAlpha() * 0.7) - normalBorderColor: Theme.outlineMedium - focusedBorderColor: Theme.primary - leftIconName: "search" - leftIconSize: Theme.iconSize - leftIconColor: Theme.surfaceVariantText - leftIconFocusedColor: Theme.primary - showClearButton: true - textColor: Theme.surfaceText - font.pixelSize: Theme.fontSizeLarge - enabled: spotlightOpen - placeholderText: "Search applications..." - ignoreLeftRightKeys: true - keyForwardTargets: [spotlightKeyHandler] - text: appLauncher.searchQuery - onTextEdited: { - appLauncher.searchQuery = text + target: "spotlight" + } + + Popup { + id: contextMenu + + property var currentApp: null + + function show(x, y, app) { + currentApp = app; + if (!contextMenu.parent && typeof Overlay !== "undefined" && Overlay.overlay) + contextMenu.parent = Overlay.overlay; + + const menuWidth = 180; + const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2; + const screenWidth = Screen.width; + const screenHeight = Screen.height; + let finalX = x; + let finalY = y; + if (x + menuWidth > screenWidth - 20) + finalX = x - menuWidth; + + if (y + menuHeight > screenHeight - 20) + finalY = y - menuHeight; + + contextMenu.x = Math.max(20, finalX); + contextMenu.y = Math.max(20, finalY); + open(); + } + + width: 180 + height: menuColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: false + closePolicy: Popup.CloseOnEscape + onClosed: { + closePolicy = Popup.CloseOnEscape; + } + onOpened: { + outsideClickTimer.start(); + } + + Timer { + id: outsideClickTimer + + interval: 100 + onTriggered: { + contextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside; } - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - hide() - event.accepted = true - } else if ((event.key === Qt.Key_Return - || event.key === Qt.Key_Enter) - && text.length > 0) { - if (appLauncher.keyboardNavigationActive - && appLauncher.model.count > 0) - appLauncher.launchSelected() - else if (appLauncher.model.count > 0) - appLauncher.launchApp(appLauncher.model.get(0)) - event.accepted = true - } else if (event.key === Qt.Key_Down || event.key - === Qt.Key_Up || event.key === Qt.Key_Left || event.key - === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) { - event.accepted = false - } + } + + background: Rectangle { + color: "transparent" + } + + contentItem: Rectangle { + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + Column { + id: menuColumn + + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 1 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: pinMouseArea.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.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: { + if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) + return "push_pin"; + + var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""; + return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"; } + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } - Connections { - function onSpotlightOpenChanged() { - if (spotlightModal.spotlightOpen) { - Qt.callLater(function () { - searchField.forceActiveFocus() - }) - } - } + StyledText { + text: { + if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) + return "Pin to Dock"; - target: spotlightModal - } - } + var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""; + return SessionData.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock"; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } - Row { - spacing: Theme.spacingXS - visible: appLauncher.model.count > 0 - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - width: 36 - height: 36 - radius: Theme.cornerRadius - color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent" - border.color: appLauncher.viewMode === "list" ? Theme.primarySelected : "transparent" - border.width: 1 - - DankIcon { - anchors.centerIn: parent - name: "view_list" - size: 18 - color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText - } - - MouseArea { - id: listViewArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - appLauncher.setViewMode("list") - } - } - } - - Rectangle { - width: 36 - height: 36 - radius: Theme.cornerRadius - color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent" - border.color: appLauncher.viewMode === "grid" ? Theme.primarySelected : "transparent" - border.width: 1 - - DankIcon { - anchors.centerIn: parent - name: "grid_view" - size: 18 - color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText - } - - MouseArea { - id: gridViewArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - appLauncher.setViewMode("grid") - } - } - } - } - } - - Rectangle { - id: resultsContainer - - width: parent.width - height: parent.height - y - radius: Theme.cornerRadius - color: Theme.surfaceLight - border.color: Theme.outlineLight - border.width: 1 - - DankListView { - id: resultsList - - property int itemHeight: 60 - property int iconSize: 40 - property bool showDescription: true - property int itemSpacing: Theme.spacingS - property bool hoverUpdatesSelection: false - property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive - - signal keyboardNavigationReset - signal itemClicked(int index, var modelData) - signal itemHovered(int index) - signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) - - function ensureVisible(index) { - if (index < 0 || index >= count) - return - - var itemY = index * (itemHeight + itemSpacing) - var itemBottom = itemY + itemHeight - if (itemY < contentY) - contentY = itemY - else if (itemBottom > contentY + height) - contentY = itemBottom - height - } - - anchors.fill: parent - anchors.margins: Theme.spacingS - visible: appLauncher.viewMode === "list" - model: appLauncher.model - currentIndex: appLauncher.selectedIndex - clip: true - spacing: itemSpacing - focus: true - interactive: true - cacheBuffer: Math.max(0, Math.min(height * 2, 1000)) - reuseItems: true - - onCurrentIndexChanged: { - if (keyboardNavigationActive) - ensureVisible(currentIndex) - } - - onItemClicked: function (index, modelData) { - appLauncher.launchApp(modelData) - } - onItemHovered: function (index) { - appLauncher.selectedIndex = index - } - onItemRightClicked: function (index, modelData, mouseX, mouseY) { - contextMenu.show(mouseX, mouseY, modelData) - } - onKeyboardNavigationReset: { - appLauncher.keyboardNavigationActive = false - } - - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AlwaysOn - } - - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AlwaysOff - } - - delegate: Rectangle { - width: ListView.view.width - height: resultsList.itemHeight - radius: Theme.cornerRadius - color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) - border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium - border.width: ListView.isCurrentItem ? 2 : 1 - - Row { - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingL - - Item { - width: resultsList.iconSize - height: resultsList.iconSize - anchors.verticalCenter: parent.verticalCenter - - IconImage { - id: listIconImg - - anchors.fill: parent - source: (model.icon) ? Quickshell.iconPath( - model.icon, - SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : "" - smooth: true - asynchronous: true - visible: status === Image.Ready - } - - Rectangle { - anchors.fill: parent - visible: !listIconImg.visible - color: Theme.surfaceLight - radius: Theme.cornerRadius - border.width: 1 - border.color: Theme.primarySelected - - StyledText { - anchors.centerIn: parent - text: (model.name - && model.name.length > 0) ? model.name.charAt( - 0).toUpperCase( - ) : "A" - font.pixelSize: resultsList.iconSize * 0.4 - color: Theme.primary - font.weight: Font.Bold } - } - } - Column { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - resultsList.iconSize - Theme.spacingL - spacing: Theme.spacingXS + MouseArea { + id: pinMouseArea - StyledText { - width: parent.width - text: model.name || "" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - elide: Text.ElideRight - } + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) + return ; - StyledText { - width: parent.width - text: model.comment || "Application" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - elide: Text.ElideRight - visible: resultsList.showDescription && model.comment - && model.comment.length > 0 - } - } - } - - MouseArea { - id: listMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - z: 10 - onEntered: { - if (resultsList.hoverUpdatesSelection - && !resultsList.keyboardNavigationActive) - resultsList.currentIndex = index - - resultsList.itemHovered(index) - } - onPositionChanged: { - resultsList.keyboardNavigationReset() - } - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - resultsList.itemClicked(index, model) - } else if (mouse.button === Qt.RightButton) { - var globalPos = mapToGlobal(mouse.x, mouse.y) - resultsList.itemRightClicked(index, model, - globalPos.x, - globalPos.y) - } - } - } - } - } - - DankGridView { - id: resultsGrid - - property int currentIndex: appLauncher.selectedIndex - property int columns: 4 - property bool adaptiveColumns: false - property int minCellWidth: 120 - property int maxCellWidth: 160 - property int cellPadding: 8 - property real iconSizeRatio: 0.55 - property int maxIconSize: 48 - property int minIconSize: 32 - property bool hoverUpdatesSelection: false - property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive - property int baseCellWidth: adaptiveColumns ? Math.max( - minCellWidth, - Math.min( - maxCellWidth, - width / columns)) : (width - Theme.spacingS * 2) / columns - property int baseCellHeight: baseCellWidth + 20 - property int actualColumns: adaptiveColumns ? Math.floor( - width / cellWidth) : columns - property int remainingSpace: width - (actualColumns * cellWidth) - - signal keyboardNavigationReset - signal itemClicked(int index, var modelData) - signal itemHovered(int index) - signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) - - function ensureVisible(index) { - if (index < 0 || index >= count) - return - - var itemY = Math.floor(index / actualColumns) * cellHeight - var itemBottom = itemY + cellHeight - if (itemY < contentY) - contentY = itemY - else if (itemBottom > contentY + height) - contentY = itemBottom - height - } - - anchors.fill: parent - anchors.margins: Theme.spacingS - visible: appLauncher.viewMode === "grid" - model: appLauncher.model - clip: true - cellWidth: baseCellWidth - cellHeight: baseCellHeight - leftMargin: Math.max(Theme.spacingS, remainingSpace / 2) - rightMargin: leftMargin - focus: true - interactive: true - cacheBuffer: Math.max(0, Math.min(height * 2, 1000)) - reuseItems: true - - onCurrentIndexChanged: { - if (keyboardNavigationActive) - ensureVisible(currentIndex) - } - - onItemClicked: function (index, modelData) { - appLauncher.launchApp(modelData) - } - onItemHovered: function (index) { - appLauncher.selectedIndex = index - } - onItemRightClicked: function (index, modelData, mouseX, mouseY) { - contextMenu.show(mouseX, mouseY, modelData) - } - onKeyboardNavigationReset: { - appLauncher.keyboardNavigationActive = false - } - - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } - - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AlwaysOff - } - - delegate: Rectangle { - width: resultsGrid.cellWidth - resultsGrid.cellPadding - height: resultsGrid.cellHeight - resultsGrid.cellPadding - radius: Theme.cornerRadius - color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) - border.color: resultsGrid.currentIndex - === index ? Theme.primarySelected : Theme.outlineMedium - border.width: resultsGrid.currentIndex === index ? 2 : 1 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingS - - Item { - property int iconSize: Math.min( - resultsGrid.maxIconSize, Math.max( - resultsGrid.minIconSize, - resultsGrid.cellWidth * resultsGrid.iconSizeRatio)) - - width: iconSize - height: iconSize - anchors.horizontalCenter: parent.horizontalCenter - - IconImage { - id: gridIconImg - - anchors.fill: parent - source: (model.icon) ? Quickshell.iconPath( - model.icon, - SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : "" - smooth: true - asynchronous: true - visible: status === Image.Ready - } - - Rectangle { - anchors.fill: parent - visible: !gridIconImg.visible - color: Theme.surfaceLight - radius: Theme.cornerRadius - border.width: 1 - border.color: Theme.primarySelected - - StyledText { - anchors.centerIn: parent - text: (model.name - && model.name.length > 0) ? model.name.charAt( - 0).toUpperCase( - ) : "A" - font.pixelSize: Math.min(28, parent.width * 0.5) - color: Theme.primary - font.weight: Font.Bold + var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""; + if (SessionData.isPinnedApp(appId)) + SessionData.removePinnedApp(appId); + else + SessionData.addPinnedApp(appId); + contextMenu.close(); + } } - } + } - StyledText { - anchors.horizontalCenter: parent.horizontalCenter - width: resultsGrid.cellWidth - 12 - text: model.name || "" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - maximumLineCount: 2 - wrapMode: Text.WordWrap + Rectangle { + width: parent.width - Theme.spacingS * 2 + height: 5 + 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) + } + } - } - MouseArea { - id: gridMouseArea + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - z: 10 - onEntered: { - if (resultsGrid.hoverUpdatesSelection - && !resultsGrid.keyboardNavigationActive) - resultsGrid.currentIndex = index + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "launch" + size: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Launch" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + + } + + MouseArea { + id: launchMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (contextMenu.currentApp && contentLoader.item && contentLoader.item.appLauncher) + contentLoader.item.appLauncher.launchApp(contextMenu.currentApp); + + contextMenu.close(); + } + } - resultsGrid.itemHovered(index) } - onPositionChanged: { - resultsGrid.keyboardNavigationReset() - } - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - resultsGrid.itemClicked(index, model) - } else if (mouse.button === Qt.RightButton) { - var globalPos = mapToGlobal(mouse.x, mouse.y) - resultsGrid.itemRightClicked(index, model, - globalPos.x, - globalPos.y) - } - } - } - } - } - } - } - } - } - Popup { - id: contextMenu - - property var currentApp: null - - function show(x, y, app) { - currentApp = app - if (!contextMenu.parent && typeof Overlay !== "undefined" - && Overlay.overlay) - contextMenu.parent = Overlay.overlay - - const menuWidth = 180 - const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2 - const screenWidth = Screen.width - const screenHeight = Screen.height - let finalX = x - let finalY = y - if (x + menuWidth > screenWidth - 20) - finalX = x - menuWidth - - if (y + menuHeight > screenHeight - 20) - finalY = y - menuHeight - - contextMenu.x = Math.max(20, finalX) - contextMenu.y = Math.max(20, finalY) - open() - } - - width: 180 - height: menuColumn.implicitHeight + Theme.spacingS * 2 - padding: 0 - modal: false - closePolicy: Popup.CloseOnEscape - onClosed: { - closePolicy = Popup.CloseOnEscape - } - onOpened: { - outsideClickTimer.start() - } - - Timer { - id: outsideClickTimer - interval: 100 - onTriggered: { - contextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside - } - } - - background: Rectangle { - color: "transparent" - } - - contentItem: Rectangle { - color: Theme.popupBackground() - radius: Theme.cornerRadius - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, - Theme.outline.b, 0.08) - border.width: 1 - - Column { - id: menuColumn - anchors.fill: parent - anchors.margins: Theme.spacingS - spacing: 1 - - Rectangle { - width: parent.width - height: 32 - radius: Theme.cornerRadius - color: pinMouseArea.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.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - DankIcon { - name: { - if (!contextMenu.currentApp - || !contextMenu.currentApp.desktopEntry) - return "push_pin" - var appId = contextMenu.currentApp.desktopEntry.id - || contextMenu.currentApp.desktopEntry.execString || "" - return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin" - } - size: Theme.iconSize - 2 - color: Theme.surfaceText - opacity: 0.7 - anchors.verticalCenter: parent.verticalCenter } - StyledText { - text: { - if (!contextMenu.currentApp - || !contextMenu.currentApp.desktopEntry) - return "Pin to Dock" - var appId = contextMenu.currentApp.desktopEntry.id - || contextMenu.currentApp.desktopEntry.execString || "" - return SessionData.isPinnedApp( - appId) ? "Unpin from Dock" : "Pin to Dock" - } - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Normal - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: pinMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (!contextMenu.currentApp - || !contextMenu.currentApp.desktopEntry) - return - var appId = contextMenu.currentApp.desktopEntry.id - || contextMenu.currentApp.desktopEntry.execString || "" - if (SessionData.isPinnedApp(appId)) { - SessionData.removePinnedApp(appId) - } else { - SessionData.addPinnedApp(appId) - } - contextMenu.close() - } - } } - Rectangle { - width: parent.width - Theme.spacingS * 2 - height: 5 - 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 { - width: parent.width - height: 32 - radius: Theme.cornerRadius - color: launchMouseArea.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.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - DankIcon { - name: "launch" - size: Theme.iconSize - 2 - color: Theme.surfaceText - opacity: 0.7 - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "Launch" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Normal - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: launchMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (contextMenu.currentApp) { - appLauncher.launchApp(contextMenu.currentApp) - } - contextMenu.close() - } - } - } - } } - } - content: spotlightContent }