1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

launcher: add name, icon, description overrides + hide/unhide options

- convenient helpers without needing to make .desktop overrides
fixes #1329
This commit is contained in:
bbedward
2026-01-18 20:30:50 -05:00
parent 7cb39f00ad
commit 0f6ae11c3d
14 changed files with 1359 additions and 397 deletions

View File

@@ -1,5 +1,12 @@
This file is more of a quick reference so I know what to account for before next releases. This file is more of a quick reference so I know what to account for before next releases.
# 1.4.0
- Overhauled system monitor, graphs, styling
- dbus API for plugins, KDEConnect
- new dank16 algorithm
- launcher actions, customize env, args, name, icon
# 1.2.0 # 1.2.0
- Added clipboard and clipboard history integration - Added clipboard and clipboard history integration

View File

@@ -21,6 +21,7 @@ Singleton {
property bool _isReadOnly: false property bool _isReadOnly: false
property bool _hasUnsavedChanges: false property bool _hasUnsavedChanges: false
property var _loadedSessionSnapshot: null property var _loadedSessionSnapshot: null
readonly property var _hooks: ({})
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl) readonly property string _stateDir: Paths.strip(_stateUrl)
@@ -102,6 +103,10 @@ Singleton {
property string weatherLocation: "New York, NY" property string weatherLocation: "New York, NY"
property string weatherCoordinates: "40.7128,-74.0060" property string weatherCoordinates: "40.7128,-74.0060"
property var hiddenApps: []
property var appOverrides: ({})
property bool searchAppActions: true
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
loadSettings(); loadSettings();
@@ -261,6 +266,10 @@ Singleton {
_checkSessionWritable(); _checkSessionWritable();
} }
function set(key, value) {
Spec.set(root, key, value, saveSettings, _hooks);
}
function migrateFromUndefinedToV1(settings) { function migrateFromUndefinedToV1(settings) {
console.info("SessionData: Migrating configuration from undefined to version 1"); console.info("SessionData: Migrating configuration from undefined to version 1");
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
@@ -906,6 +915,61 @@ Singleton {
saveSettings(); saveSettings();
} }
function hideApp(appId) {
if (!appId)
return;
const current = [...hiddenApps];
if (current.indexOf(appId) === -1) {
current.push(appId);
hiddenApps = current;
saveSettings();
}
}
function showApp(appId) {
if (!appId)
return;
hiddenApps = hiddenApps.filter(id => id !== appId);
saveSettings();
}
function isAppHidden(appId) {
return appId && hiddenApps.indexOf(appId) !== -1;
}
function setAppOverride(appId, overrides) {
if (!appId)
return;
const newOverrides = Object.assign({}, appOverrides);
if (!overrides || Object.keys(overrides).length === 0) {
delete newOverrides[appId];
} else {
newOverrides[appId] = overrides;
}
appOverrides = newOverrides;
saveSettings();
}
function getAppOverride(appId) {
if (!appId)
return null;
return appOverrides[appId] || null;
}
function clearAppOverride(appId) {
if (!appId)
return;
const newOverrides = Object.assign({}, appOverrides);
delete newOverrides[appId];
appOverrides = newOverrides;
saveSettings();
}
function setSearchAppActions(enabled) {
searchAppActions = enabled;
saveSettings();
}
function syncWallpaperForCurrentMode() { function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) if (!perModeWallpaper)
return; return;

View File

@@ -55,9 +55,23 @@ var SPEC = {
enabledGpuPciIds: { def: [] }, enabledGpuPciIds: { def: [] },
wifiDeviceOverride: { def: "" }, wifiDeviceOverride: { def: "" },
weatherHourlyDetailed: { def: true } weatherHourlyDetailed: { def: true },
hiddenApps: { def: [] },
appOverrides: { def: {} },
searchAppActions: { def: true }
}; };
function getValidKeys() { function getValidKeys() {
return Object.keys(SPEC).concat(["configVersion"]); return Object.keys(SPEC).concat(["configVersion"]);
} }
function set(root, key, value, saveFn, hooks) {
if (!(key in SPEC)) return;
root[key] = value;
var hookName = SPEC[key].onChange;
if (hookName && hooks && hooks[hookName]) {
hooks[hookName](root);
}
saveFn();
}

View File

@@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Modals.Spotlight import qs.Modals.Spotlight
@@ -19,6 +20,10 @@ Item {
property string searchMode: "apps" property string searchMode: "apps"
property bool usePopupContextMenu: false property bool usePopupContextMenu: false
property bool editMode: false
property var editingApp: null
property string editAppId: ""
function resetScroll() { function resetScroll() {
if (searchMode === "apps") { if (searchMode === "apps") {
resultsView.resetScroll(); resultsView.resetScroll();
@@ -43,6 +48,49 @@ Item {
} }
} }
function openEditMode(app) {
if (!app)
return;
editingApp = app;
editAppId = app.id || app.execString || app.exec || "";
const existing = SessionData.getAppOverride(editAppId);
editNameField.text = existing?.name || "";
editIconField.text = existing?.icon || "";
editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || "";
editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus());
}
function closeEditMode() {
editMode = false;
editingApp = null;
editAppId = "";
Qt.callLater(() => searchField.forceActiveFocus());
}
function saveAppOverride() {
const 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();
SessionData.setAppOverride(editAppId, override);
closeEditMode();
}
function resetAppOverride() {
SessionData.clearAppOverride(editAppId);
closeEditMode();
}
onSearchModeChanged: { onSearchModeChanged: {
if (searchMode === "files") { if (searchMode === "files") {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false;
@@ -55,10 +103,16 @@ Item {
focus: true focus: true
clip: false clip: false
Keys.onPressed: event => { Keys.onPressed: event => {
if (editMode) {
if (event.key === Qt.Key_Escape) {
closeEditMode();
event.accepted = true;
}
return;
}
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (parentModal) if (parentModal)
parentModal.hide(); parentModal.hide();
event.accepted = true; event.accepted = true;
} else if (event.key === Qt.Key_Down) { } else if (event.key === Qt.Key_Down) {
if (searchMode === "apps") { if (searchMode === "apps") {
@@ -155,7 +209,6 @@ Item {
if (searchMode === "apps" && appLauncher.model.count > 0) { if (searchMode === "apps" && appLauncher.model.count > 0) {
const selectedApp = appLauncher.model.get(appLauncher.selectedIndex); const selectedApp = appLauncher.model.get(appLauncher.selectedIndex);
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item; const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
if (selectedApp && menu && resultsView) { if (selectedApp && menu && resultsView) {
const itemPos = resultsView.getSelectedItemPosition(); const itemPos = resultsView.getSelectedItemPosition();
const contentPos = resultsView.mapToItem(spotlightKeyHandler, itemPos.x, itemPos.y); const contentPos = resultsView.mapToItem(spotlightKeyHandler, itemPos.x, itemPos.y);
@@ -168,7 +221,6 @@ Item {
AppLauncher { AppLauncher {
id: appLauncher id: appLauncher
viewMode: SettingsData.spotlightModalViewMode viewMode: SettingsData.spotlightModalViewMode
gridColumns: SettingsData.appLauncherGridColumns gridColumns: SettingsData.appLauncherGridColumns
onAppLaunched: () => { onAppLaunched: () => {
@@ -185,7 +237,6 @@ Item {
FileSearchController { FileSearchController {
id: fileSearchController id: fileSearchController
onFileOpened: () => { onFileOpened: () => {
if (parentModal) if (parentModal)
parentModal.hide(); parentModal.hide();
@@ -197,7 +248,6 @@ Item {
SpotlightContextMenuPopup { SpotlightContextMenuPopup {
id: popupContextMenu id: popupContextMenu
parent: spotlightKeyHandler parent: spotlightKeyHandler
appLauncher: spotlightKeyHandler.appLauncher appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler parentHandler: spotlightKeyHandler
@@ -231,20 +281,37 @@ Item {
target: parentModal target: parentModal
function onSpotlightOpenChanged() { function onSpotlightOpenChanged() {
if (parentModal && !parentModal.spotlightOpen) { if (parentModal && !parentModal.spotlightOpen) {
if (layerContextMenuLoader.item) { if (layerContextMenuLoader.item)
layerContextMenuLoader.item.hide(); layerContextMenuLoader.item.hide();
}
popupContextMenu.hide(); popupContextMenu.hide();
if (editMode)
closeEditMode();
} }
} }
enabled: parentModal !== null enabled: parentModal !== null
} }
Connections {
target: popupContextMenu
function onEditAppRequested(app) {
spotlightKeyHandler.openEditMode(app);
}
}
Connections {
target: layerContextMenuLoader.item
function onEditAppRequested(app) {
spotlightKeyHandler.openEditMode(app);
}
enabled: layerContextMenuLoader.item !== null
}
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
spacing: Theme.spacingM spacing: Theme.spacingM
clip: false clip: false
visible: !editMode
Item { Item {
id: searchRow id: searchRow
@@ -275,18 +342,14 @@ Item {
ignoreTabKeys: true ignoreTabKeys: true
keyForwardTargets: [spotlightKeyHandler] keyForwardTargets: [spotlightKeyHandler]
onTextChanged: { onTextChanged: {
if (searchMode === "apps") { if (searchMode === "apps")
appLauncher.searchQuery = text; appLauncher.searchQuery = text;
} }
} onTextEdited: updateSearchMode()
onTextEdited: {
updateSearchMode();
}
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (parentModal) if (parentModal)
parentModal.hide(); parentModal.hide();
event.accepted = true; event.accepted = true;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) { } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (searchMode === "apps") { if (searchMode === "apps") {
@@ -334,13 +397,10 @@ Item {
MouseArea { MouseArea {
id: listViewArea id: listViewArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: appLauncher.setViewMode("list")
appLauncher.setViewMode("list");
}
} }
} }
@@ -359,13 +419,10 @@ Item {
MouseArea { MouseArea {
id: gridViewArea id: gridViewArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: appLauncher.setViewMode("grid")
appLauncher.setViewMode("grid");
}
} }
} }
} }
@@ -379,7 +436,6 @@ Item {
Rectangle { Rectangle {
id: filenameFilterButton id: filenameFilterButton
width: 36 width: 36
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -394,13 +450,10 @@ Item {
MouseArea { MouseArea {
id: filenameFilterArea id: filenameFilterArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: fileSearchController.searchField = "filename"
fileSearchController.searchField = "filename";
}
onEntered: { onEntered: {
filenameTooltipLoader.active = true; filenameTooltipLoader.active = true;
Qt.callLater(() => { Qt.callLater(() => {
@@ -413,7 +466,6 @@ Item {
onExited: { onExited: {
if (filenameTooltipLoader.item) if (filenameTooltipLoader.item)
filenameTooltipLoader.item.hide(); filenameTooltipLoader.item.hide();
filenameTooltipLoader.active = false; filenameTooltipLoader.active = false;
} }
} }
@@ -421,7 +473,6 @@ Item {
Rectangle { Rectangle {
id: contentFilterButton id: contentFilterButton
width: 36 width: 36
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -436,13 +487,10 @@ Item {
MouseArea { MouseArea {
id: contentFilterArea id: contentFilterArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: fileSearchController.searchField = "body"
fileSearchController.searchField = "body";
}
onEntered: { onEntered: {
contentTooltipLoader.active = true; contentTooltipLoader.active = true;
Qt.callLater(() => { Qt.callLater(() => {
@@ -455,7 +503,6 @@ Item {
onExited: { onExited: {
if (contentTooltipLoader.item) if (contentTooltipLoader.item)
contentTooltipLoader.item.hide(); contentTooltipLoader.item.hide();
contentTooltipLoader.active = false; contentTooltipLoader.active = false;
} }
} }
@@ -474,13 +521,10 @@ Item {
anchors.fill: parent anchors.fill: parent
appLauncher: spotlightKeyHandler.appLauncher appLauncher: spotlightKeyHandler.appLauncher
visible: searchMode === "apps" visible: searchMode === "apps"
onItemRightClicked: (index, modelData, mouseX, mouseY) => { onItemRightClicked: (index, modelData, mouseX, mouseY) => {
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item; const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
if (menu?.show) { if (menu?.show) {
const isPopup = menu.contentItem !== undefined; const isPopup = menu.contentItem !== undefined;
if (isPopup) { if (isPopup) {
const localPos = popupContextMenu.parent.mapFromItem(null, mouseX, mouseY); const localPos = popupContextMenu.parent.mapFromItem(null, mouseX, mouseY);
menu.show(localPos.x, localPos.y, modelData, false); menu.show(localPos.x, localPos.y, modelData, false);
@@ -500,16 +544,320 @@ Item {
} }
} }
FocusScope {
id: editView
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: editMode
focus: editMode
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Escape:
closeEditMode();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
return;
case Qt.Key_S:
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
return;
case Qt.Key_R:
if ((event.modifiers & Qt.ControlModifier) && SessionData.getAppOverride(editAppId) !== null) {
resetAppOverride();
event.accepted = true;
}
return;
}
}
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: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://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
height: 44
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
height: 44
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
height: 44
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
height: 44
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
height: 44
placeholderText: "--flag --option=value"
keyNavigationTab: editNameField
keyNavigationBacktab: editEnvVarsField
}
}
}
}
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()
}
}
}
}
}
Loader { Loader {
id: filenameTooltipLoader id: filenameTooltipLoader
active: false active: false
sourceComponent: DankTooltip {} sourceComponent: DankTooltip {}
} }
Loader { Loader {
id: contentTooltipLoader id: contentTooltipLoader
active: false active: false
sourceComponent: DankTooltip {} sourceComponent: DankTooltip {}
} }

View File

@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.Spotlight import qs.Modals.Spotlight
@@ -19,6 +18,8 @@ PanelWindow {
property real menuPositionX: 0 property real menuPositionX: 0
property real menuPositionY: 0 property real menuPositionY: 0
signal editAppRequested(var app)
readonly property real shadowBuffer: 5 readonly property real shadowBuffer: 5
screen: parentModal?.effectiveScreen screen: parentModal?.effectiveScreen
@@ -106,6 +107,7 @@ PanelWindow {
} }
onHideRequested: root.hide() onHideRequested: root.hide()
onEditAppRequested: app => root.editAppRequested(app)
} }
MouseArea { MouseArea {

View File

@@ -68,6 +68,27 @@ Item {
hideRequested(); hideRequested();
} }
readonly property bool isRegularApp: desktopEntry && !currentApp?.isPlugin && !currentApp?.isCore && !currentApp?.isAction && !currentApp?.isBuiltInLauncher
signal editAppRequested(var app)
function hideCurrentApp() {
if (!desktopEntry)
return;
const appId = desktopEntry.id || desktopEntry.execString || "";
SessionData.hideApp(appId);
if (appLauncher)
appLauncher.updateFilteredModel();
hideRequested();
}
function editCurrentApp() {
if (!desktopEntry)
return;
editAppRequested(desktopEntry);
hideRequested();
}
readonly property var menuItems: { readonly property var menuItems: {
const items = []; const items = [];
@@ -103,6 +124,21 @@ Item {
action: togglePin 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 (desktopEntry && desktopEntry.actions) { if (desktopEntry && desktopEntry.actions) {
items.push({ items.push({
type: "separator" type: "separator"

View File

@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell
import qs.Common import qs.Common
import qs.Modals.Spotlight import qs.Modals.Spotlight
@@ -11,6 +10,8 @@ Popup {
property var parentHandler: null property var parentHandler: null
property var searchField: null property var searchField: null
signal editAppRequested(var app)
function show(x, y, app, fromKeyboard) { function show(x, y, app, fromKeyboard) {
fromKeyboard = fromKeyboard || false; fromKeyboard = fromKeyboard || false;
menuContent.currentApp = app; menuContent.currentApp = app;
@@ -53,7 +54,7 @@ Popup {
if (parentHandler) { if (parentHandler) {
parentHandler.enabled = true; parentHandler.enabled = true;
} }
if (searchField) { if (searchField?.visible) {
Qt.callLater(() => { Qt.callLater(() => {
searchField.forceActiveFocus(); searchField.forceActiveFocus();
}); });
@@ -84,5 +85,6 @@ Popup {
id: menuContent id: menuContent
appLauncher: root.appLauncher appLauncher: root.appLauncher
onHideRequested: root.hide() onHideRequested: root.hide()
onEditAppRequested: app => root.editAppRequested(app)
} }
} }

View File

@@ -65,6 +65,20 @@ DankModal {
}); });
} }
function showWithEditApp(app) {
openedFromOverview = false;
isClosing = false;
resetContent();
spotlightOpen = true;
open();
Qt.callLater(() => {
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
if (spotlightContent?.openEditMode)
spotlightContent.openEditMode(app);
});
}
function hide() { function hide() {
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;

View File

@@ -1,11 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.Spotlight import qs.Modals.Spotlight
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets import qs.Widgets
DankPopout { DankPopout {
@@ -15,6 +11,63 @@ DankPopout {
property string searchMode: "apps" property string searchMode: "apps"
property alias fileSearch: fileSearchController property alias fileSearch: fileSearchController
property bool editMode: false
property var editingApp: null
property string editAppId: ""
function openEditMode(app) {
if (!app)
return;
editingApp = app;
editAppId = app.id || app.execString || app.exec || "";
const existing = SessionData.getAppOverride(editAppId);
if (contentLoader.item) {
contentLoader.item.searchField.focus = false;
contentLoader.item.editNameField.text = existing?.name || "";
contentLoader.item.editIconField.text = existing?.icon || "";
contentLoader.item.editCommentField.text = existing?.comment || "";
contentLoader.item.editEnvVarsField.text = existing?.envVars || "";
contentLoader.item.editExtraFlagsField.text = existing?.extraFlags || "";
}
editMode = true;
Qt.callLater(() => {
if (contentLoader.item?.editNameField)
contentLoader.item.editNameField.forceActiveFocus();
});
}
function closeEditMode() {
editMode = false;
editingApp = null;
editAppId = "";
Qt.callLater(() => {
if (contentLoader.item?.searchField)
contentLoader.item.searchField.forceActiveFocus();
});
}
function saveAppOverride() {
const override = {};
if (contentLoader.item) {
if (contentLoader.item.editNameField.text.trim())
override.name = contentLoader.item.editNameField.text.trim();
if (contentLoader.item.editIconField.text.trim())
override.icon = contentLoader.item.editIconField.text.trim();
if (contentLoader.item.editCommentField.text.trim())
override.comment = contentLoader.item.editCommentField.text.trim();
if (contentLoader.item.editEnvVarsField.text.trim())
override.envVars = contentLoader.item.editEnvVarsField.text.trim();
if (contentLoader.item.editExtraFlagsField.text.trim())
override.extraFlags = contentLoader.item.editExtraFlagsField.text.trim();
}
SessionData.setAppOverride(editAppId, override);
closeEditMode();
}
function resetAppOverride() {
SessionData.clearAppOverride(editAppId);
closeEditMode();
}
function updateSearchMode(text) { function updateSearchMode(text) {
if (text.startsWith("/")) { if (text.startsWith("/")) {
@@ -42,16 +95,25 @@ DankPopout {
popupHeight: 600 popupHeight: 600
triggerWidth: 40 triggerWidth: 40
positioning: "" positioning: ""
contentHandlesKeys: editMode
onBackgroundClicked: { onBackgroundClicked: {
if (contextMenu.visible) { if (contextMenu.visible) {
contextMenu.close(); contextMenu.close();
return;
}
if (editMode) {
closeEditMode();
return;
} }
close(); close();
} }
onOpened: { onOpened: {
searchMode = "apps"; searchMode = "apps";
editMode = false;
editingApp = null;
editAppId = "";
appLauncher.ensureInitialized(); appLauncher.ensureInitialized();
appLauncher.searchQuery = ""; appLauncher.searchQuery = "";
appLauncher.selectedIndex = 0; appLauncher.selectedIndex = 0;
@@ -100,8 +162,45 @@ DankPopout {
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
property alias searchField: searchField property alias searchField: searchField
property alias keyHandler: keyHandler
property alias editNameField: editNameField
property alias editIconField: editIconField
property alias editCommentField: editCommentField
property alias editEnvVarsField: editEnvVarsField
property alias editExtraFlagsField: editExtraFlagsField
focus: true
color: "transparent" color: "transparent"
Keys.onPressed: function (event) {
if (appDrawerPopout.editMode) {
switch (event.key) {
case Qt.Key_Escape:
appDrawerPopout.closeEditMode();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (event.modifiers & Qt.ControlModifier) {
appDrawerPopout.saveAppOverride();
event.accepted = true;
}
return;
case Qt.Key_S:
if (event.modifiers & Qt.ControlModifier) {
appDrawerPopout.saveAppOverride();
event.accepted = true;
}
return;
case Qt.Key_R:
if ((event.modifiers & Qt.ControlModifier) && SessionData.getAppOverride(appDrawerPopout.editAppId) !== null) {
appDrawerPopout.resetAppOverride();
event.accepted = true;
}
return;
}
}
}
radius: Theme.cornerRadius radius: Theme.cornerRadius
antialiasing: true antialiasing: true
smooth: true smooth: true
@@ -140,7 +239,7 @@ DankPopout {
id: keyHandler id: keyHandler
anchors.fill: parent anchors.fill: parent
focus: true focus: !appDrawerPopout.editMode
function selectNext() { function selectNext() {
switch (appDrawerPopout.searchMode) { switch (appDrawerPopout.searchMode) {
@@ -172,6 +271,29 @@ DankPopout {
} }
} }
function getSelectedItemPosition() {
const index = appLauncher.selectedIndex;
if (appLauncher.viewMode === "list") {
const y = index * (appList.itemHeight + appList.itemSpacing) - appList.contentY;
return Qt.point(appList.width / 2, y + appList.itemHeight / 2 + appList.y);
}
const row = Math.floor(index / appGrid.actualColumns);
const col = index % appGrid.actualColumns;
const x = col * appGrid.cellWidth + appGrid.cellWidth / 2;
const y = row * appGrid.cellHeight - appGrid.contentY + appGrid.cellHeight / 2 + appGrid.y;
return Qt.point(x, y);
}
function openContextMenuForSelected() {
if (appDrawerPopout.searchMode !== "apps" || appLauncher.model.count === 0)
return;
const selectedApp = appLauncher.model.get(appLauncher.selectedIndex);
if (!selectedApp)
return;
const pos = getSelectedItemPosition();
contextMenu.show(pos.x, pos.y, selectedApp, true);
}
readonly property var keyMappings: { readonly property var keyMappings: {
const mappings = {}; const mappings = {};
mappings[Qt.Key_Escape] = () => appDrawerPopout.close(); mappings[Qt.Key_Escape] = () => appDrawerPopout.close();
@@ -181,6 +303,8 @@ DankPopout {
mappings[Qt.Key_Enter] = () => keyHandler.activateSelected(); mappings[Qt.Key_Enter] = () => keyHandler.activateSelected();
mappings[Qt.Key_Tab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : keyHandler.selectNext(); mappings[Qt.Key_Tab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : keyHandler.selectNext();
mappings[Qt.Key_Backtab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : keyHandler.selectPrevious(); mappings[Qt.Key_Backtab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : keyHandler.selectPrevious();
mappings[Qt.Key_Menu] = () => keyHandler.openContextMenuForSelected();
mappings[Qt.Key_F10] = () => keyHandler.openContextMenuForSelected();
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") { if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
mappings[Qt.Key_Right] = () => I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow(); mappings[Qt.Key_Right] = () => I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
@@ -191,6 +315,9 @@ DankPopout {
} }
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if (appDrawerPopout.editMode)
return;
if (keyMappings[event.key]) { if (keyMappings[event.key]) {
keyMappings[event.key](); keyMappings[event.key]();
event.accepted = true; event.accepted = true;
@@ -198,9 +325,8 @@ DankPopout {
} }
const hasCtrl = event.modifiers & Qt.ControlModifier; const hasCtrl = event.modifiers & Qt.ControlModifier;
if (!hasCtrl) { if (!hasCtrl)
return; return;
}
switch (event.key) { switch (event.key) {
case Qt.Key_N: case Qt.Key_N:
@@ -234,6 +360,7 @@ DankPopout {
x: Theme.spacingS x: Theme.spacingS
y: Theme.spacingS y: Theme.spacingS
spacing: Theme.spacingS spacing: Theme.spacingS
visible: !appDrawerPopout.editMode
Item { Item {
width: parent.width width: parent.width
@@ -487,7 +614,7 @@ DankPopout {
appLauncher.launchApp(modelData); appLauncher.launchApp(modelData);
} }
onItemRightClicked: function (index, modelData, mouseX, mouseY) { onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData); contextMenu.show(mouseX, mouseY, modelData, false);
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false;
@@ -574,7 +701,7 @@ DankPopout {
appLauncher.launchApp(modelData); appLauncher.launchApp(modelData);
} }
onItemRightClicked: function (index, modelData, mouseX, mouseY) { onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData); contextMenu.show(mouseX, mouseY, modelData, false);
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false;
@@ -615,6 +742,281 @@ DankPopout {
} }
} }
Item {
id: editView
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appDrawerPopout.editMode
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: appDrawerPopout.closeEditMode()
}
}
Image {
width: 40
height: 40
source: appDrawerPopout.editingApp?.icon ? "image://icon/" + appDrawerPopout.editingApp.icon : "image://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: appDrawerPopout.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 - editButtonsRow.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
height: 44
focus: true
placeholderText: appDrawerPopout.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
height: 44
placeholderText: appDrawerPopout.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
height: 44
placeholderText: appDrawerPopout.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
height: 44
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
height: 44
placeholderText: "--flag --option=value"
keyNavigationTab: editNameField
keyNavigationBacktab: editEnvVarsField
}
}
}
}
Row {
id: editButtonsRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 90
height: 40
radius: Theme.cornerRadius
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
visible: SessionData.getAppOverride(appDrawerPopout.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: appDrawerPopout.resetAppOverride()
}
}
Rectangle {
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: appDrawerPopout.closeEditMode()
}
}
Rectangle {
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.onPrimary
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: saveButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: appDrawerPopout.saveAppOverride()
}
}
}
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
visible: contextMenu.visible visible: contextMenu.visible
@@ -624,338 +1026,21 @@ DankPopout {
} }
} }
Popup { SpotlightContextMenuPopup {
id: contextMenu id: contextMenu
property var currentApp: null parent: contentLoader.item
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null appLauncher: appLauncher
readonly property string appId: desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : "" parentHandler: contentLoader.item?.keyHandler ?? null
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId) searchField: contentLoader.item?.searchField ?? null
visible: false
function show(x, y, app) { z: 1000
currentApp = app;
let finalX = x + 4;
let finalY = y + 4;
if (contextMenu.parent) {
const parentWidth = contextMenu.parent.width;
const parentHeight = contextMenu.parent.height;
const menuWidth = contextMenu.width;
const menuHeight = contextMenu.height;
if (finalX + menuWidth > parentWidth) {
finalX = Math.max(0, parentWidth - menuWidth);
} }
if (finalY + menuHeight > parentHeight) { Connections {
finalY = Math.max(0, parentHeight - menuHeight); target: contextMenu
} function onEditAppRequested(app) {
} appDrawerPopout.openEditMode(app);
contextMenu.x = finalX;
contextMenu.y = finalY;
contextMenu.open();
}
function hide() {
contextMenu.close();
}
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
closePolicy: Popup.CloseOnPressOutside
modal: false
dim: false
background: Rectangle {
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
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
}
}
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
}
}
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: contextMenu.isPinned ? "keep_off" : "push_pin"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: contextMenu.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("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.desktopEntry) {
return;
}
if (contextMenu.isPinned) {
SessionData.removePinnedApp(contextMenu.appId);
} else {
SessionData.addPinnedApp(contextMenu.appId);
}
contextMenu.hide();
}
}
}
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)
}
}
Repeater {
model: contextMenu.desktopEntry && contextMenu.desktopEntry.actions ? contextMenu.desktopEntry.actions : []
Rectangle {
width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: actionRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize - 2
height: Theme.iconSize - 2
visible: modelData.icon && modelData.icon !== ""
IconImage {
anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
StyledText {
text: modelData.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: actionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && contextMenu.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.desktopEntry, modelData);
if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp);
}
}
contextMenu.hide();
}
}
}
}
Rectangle {
visible: contextMenu.desktopEntry && contextMenu.desktopEntry.actions && contextMenu.desktopEntry.actions.length > 0
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: I18n.tr("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.hide();
}
}
}
Rectangle {
visible: SessionService.nvidiaCommand
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 {
visible: SessionService.nvidiaCommand
width: parent.width
height: 32
radius: Theme.cornerRadius
color: nvidiaMouseArea.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: "memory"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch on dGPU")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: nvidiaMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.desktopEntry, true);
if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp);
}
}
contextMenu.hide();
}
}
}
} }
} }
} }

View File

@@ -72,6 +72,16 @@ Item {
} }
} }
Connections {
target: SessionData
function onHiddenAppsChanged() {
updateFilteredModel();
}
function onAppOverridesChanged() {
updateFilteredModel();
}
}
function updateFilteredModel() { function updateFilteredModel() {
if (suppressUpdatesWhileLaunching) { if (suppressUpdatesWhileLaunching) {
suppressUpdatesWhileLaunching = false; suppressUpdatesWhileLaunching = false;
@@ -124,7 +134,7 @@ Item {
emptyTriggerItems = emptyTriggerItems.concat(items); emptyTriggerItems = emptyTriggerItems.concat(items);
}); });
const coreItems = AppSearchService.getCoreApps(""); const coreItems = AppSearchService.getCoreApps("");
apps = AppSearchService.applications.concat(emptyTriggerItems).concat(coreItems); apps = AppSearchService.getVisibleApplications().concat(emptyTriggerItems).concat(coreItems);
} else { } else {
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults); apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults);
const coreItems = AppSearchService.getCoreApps("").filter(app => app.categories.includes(selectedCategory)); const coreItems = AppSearchService.getCoreApps("").filter(app => app.categories.includes(selectedCategory));
@@ -205,6 +215,7 @@ Item {
"isPlugin": isPluginItem, "isPlugin": isPluginItem,
"isCore": app.isCore === true, "isCore": app.isCore === true,
"isBuiltInLauncher": app.isBuiltInLauncher === true, "isBuiltInLauncher": app.isBuiltInLauncher === true,
"isAction": app.isAction === true,
"appIndex": uniqueApps.length - 1, "appIndex": uniqueApps.length - 1,
"pinned": app._pinned === true "pinned": app._pinned === true
}); });
@@ -283,6 +294,13 @@ Item {
return; return;
} }
if (appData.isAction && actualApp.parentApp && actualApp.actionData) {
SessionService.launchDesktopAction(actualApp.parentApp, actualApp.actionData);
appLaunched(appData);
AppUsageHistoryData.addAppUsage(actualApp.parentApp);
return;
}
SessionService.launchDesktopEntry(actualApp); SessionService.launchDesktopEntry(actualApp);
appLaunched(appData); appLaunched(appData);
AppUsageHistoryData.addAppUsage(actualApp); AppUsageHistoryData.addAppUsage(actualApp);

View File

@@ -462,6 +462,250 @@ Item {
} }
} }
SettingsCard {
width: parent.width
iconName: "search"
title: I18n.tr("Search Options")
settingKey: "searchOptions"
SettingsToggleRow {
settingKey: "searchAppActions"
tags: ["launcher", "search", "actions", "shortcuts"]
text: I18n.tr("Search App Actions")
description: I18n.tr("Include desktop actions (shortcuts) in search results.")
checked: SessionData.searchAppActions
onToggled: checked => SessionData.setSearchAppActions(checked)
}
}
SettingsCard {
id: hiddenAppsCard
width: parent.width
iconName: "visibility_off"
title: I18n.tr("Hidden Apps")
settingKey: "hiddenApps"
property var hiddenAppsModel: {
SessionData.hiddenApps;
const apps = [];
const allApps = AppSearchService.applications || [];
for (const hiddenId of SessionData.hiddenApps) {
const app = allApps.find(a => (a.id || a.execString || a.exec) === hiddenId);
if (app) {
apps.push({
id: hiddenId,
name: app.name || hiddenId,
icon: app.icon || "",
comment: app.comment || ""
});
} else {
apps.push({
id: hiddenId,
name: hiddenId,
icon: "",
comment: ""
});
}
}
return apps.sort((a, b) => a.name.localeCompare(b.name));
}
StyledText {
width: parent.width
text: I18n.tr("Hidden apps won't appear in the launcher. Right-click an app and select 'Hide App' to hide it.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Column {
id: hiddenAppsList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: hiddenAppsCard.hiddenAppsModel
delegate: Rectangle {
width: hiddenAppsList.width
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
border.width: 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Image {
width: 24
height: 24
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable";
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: modelData.comment || modelData.id
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "visibility"
iconSize: 18
iconColor: Theme.primary
onClicked: SessionData.showApp(modelData.id)
}
}
}
StyledText {
width: parent.width
text: I18n.tr("No hidden apps.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: hiddenAppsCard.hiddenAppsModel.length === 0
}
}
}
SettingsCard {
id: appOverridesCard
width: parent.width
iconName: "edit"
title: I18n.tr("App Customizations")
settingKey: "appOverrides"
property var overridesModel: {
SessionData.appOverrides;
const items = [];
const allApps = AppSearchService.applications || [];
for (const appId in SessionData.appOverrides) {
const override = SessionData.appOverrides[appId];
const app = allApps.find(a => (a.id || a.execString || a.exec) === appId);
items.push({
id: appId,
name: override.name || app?.name || appId,
originalName: app?.name || appId,
icon: override.icon || app?.icon || "",
hasOverride: true
});
}
return items.sort((a, b) => a.name.localeCompare(b.name));
}
StyledText {
width: parent.width
text: I18n.tr("Apps with custom display name, icon, or launch options. Right-click an app and select 'Edit App' to customize.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Column {
id: overridesList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: appOverridesCard.overridesModel
delegate: Rectangle {
width: overridesList.width
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
border.width: 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Image {
width: 24
height: 24
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable";
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: modelData.originalName !== modelData.name ? modelData.originalName : modelData.id
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "delete"
iconSize: 18
iconColor: Theme.error
onClicked: SessionData.clearAppOverride(modelData.id)
}
}
}
StyledText {
width: parent.width
text: I18n.tr("No app customizations.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: appOverridesCard.overridesModel.length === 0
}
}
}
SettingsCard { SettingsCard {
id: recentAppsCard id: recentAppsCard
width: parent.width width: parent.width

View File

@@ -10,6 +10,8 @@ Singleton {
property var applications: [] property var applications: []
property var _cachedCategories: null property var _cachedCategories: null
property var _cachedVisibleApps: null
property var _hiddenAppsSet: new Set()
readonly property int maxResults: 10 readonly property int maxResults: 10
readonly property int frecencySampleSize: 10 readonly property int frecencySampleSize: 10
@@ -40,6 +42,51 @@ Singleton {
function refreshApplications() { function refreshApplications() {
applications = DesktopEntries.applications.values; applications = DesktopEntries.applications.values;
_cachedCategories = null; _cachedCategories = null;
_cachedVisibleApps = null;
}
function _rebuildHiddenSet() {
_hiddenAppsSet = new Set(SessionData.hiddenApps || []);
_cachedVisibleApps = null;
}
function isAppHidden(app) {
if (!app)
return false;
const appId = app.id || app.execString || app.exec || "";
return _hiddenAppsSet.has(appId);
}
function getVisibleApplications() {
if (_cachedVisibleApps === null) {
_cachedVisibleApps = applications.filter(app => !isAppHidden(app));
}
return _cachedVisibleApps.map(app => applyAppOverride(app));
}
Connections {
target: SessionData
function onHiddenAppsChanged() {
root._rebuildHiddenSet();
}
function onAppOverridesChanged() {
root._cachedVisibleApps = null;
}
}
function applyAppOverride(app) {
if (!app)
return app;
const appId = app.id || app.execString || app.exec || "";
const override = SessionData.getAppOverride(appId);
if (!override)
return app;
return Object.assign({}, app, {
name: override.name || app.name,
icon: override.icon || app.icon,
comment: override.comment || app.comment,
_override: override
});
} }
readonly property string dmsLogoPath: Qt.resolvedUrl("../assets/danklogo2.svg") readonly property string dmsLogoPath: Qt.resolvedUrl("../assets/danklogo2.svg")
@@ -226,7 +273,10 @@ Singleton {
} }
} }
Component.onCompleted: refreshApplications() Component.onCompleted: {
_rebuildHiddenSet();
refreshApplications();
}
function tokenize(text) { function tokenize(text) {
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(w => w.length > 0); return text.toLowerCase().trim().split(/[\s\-_]+/).filter(w => w.length > 0);
@@ -345,17 +395,17 @@ Singleton {
} }
function searchApplications(query) { function searchApplications(query) {
if (!query || query.length === 0) { if (!query || query.length === 0)
return applications; return getVisibleApplications();
}
if (applications.length === 0) if (applications.length === 0)
return []; return [];
const queryLower = query.toLowerCase().trim(); const queryLower = query.toLowerCase().trim();
const scoredApps = []; const scoredApps = [];
const results = []; const results = [];
const visibleApps = getVisibleApplications();
for (const app of applications) { for (const app of visibleApps) {
const name = (app.name || "").toLowerCase(); const name = (app.name || "").toLowerCase();
const genericName = (app.genericName || "").toLowerCase(); const genericName = (app.genericName || "").toLowerCase();
const comment = (app.comment || "").toLowerCase(); const comment = (app.comment || "").toLowerCase();
@@ -440,10 +490,58 @@ Singleton {
}); });
} }
if (SessionData.searchAppActions) {
const actionResults = searchAppActions(queryLower, visibleApps);
for (const actionResult of actionResults) {
scoredApps.push({
app: actionResult.app,
score: actionResult.score
});
}
}
scoredApps.sort((a, b) => b.score - a.score); scoredApps.sort((a, b) => b.score - a.score);
return scoredApps.slice(0, maxResults).map(item => item.app); return scoredApps.slice(0, maxResults).map(item => item.app);
} }
function searchAppActions(query, apps) {
const results = [];
for (const app of apps) {
if (!app.actions || app.actions.length === 0)
continue;
for (const action of app.actions) {
const actionName = (action.name || "").toLowerCase();
if (!actionName)
continue;
let score = 0;
if (actionName === query) {
score = 8000;
} else if (actionName.startsWith(query)) {
score = 4000;
} else if (actionName.includes(query)) {
score = 400;
}
if (score > 0) {
results.push({
app: {
name: action.name,
icon: action.icon || app.icon,
comment: app.name,
categories: app.categories || [],
isAction: true,
parentApp: app,
actionData: action
},
score: score
});
}
}
}
return results;
}
function getCategoriesForApp(app) { function getCategoriesForApp(app) {
if (!app?.categories) if (!app?.categories)
return []; return [];
@@ -525,17 +623,15 @@ Singleton {
} }
function getAppsInCategory(category) { function getAppsInCategory(category) {
if (category === I18n.tr("All")) { const visibleApps = getVisibleApplications();
return applications; if (category === I18n.tr("All"))
} return visibleApps;
// Check if it's a plugin category
const pluginItems = getPluginItems(category, ""); const pluginItems = getPluginItems(category, "");
if (pluginItems.length > 0) { if (pluginItems.length > 0)
return pluginItems; return pluginItems;
}
return applications.filter(app => { return visibleApps.filter(app => {
const appCategories = getCategoriesForApp(app); const appCategories = getCategoriesForApp(app);
return appCategories.includes(category); return appCategories.includes(category);
}); });

View File

@@ -182,17 +182,44 @@ Singleton {
return /[;&|<>()$`\\"']/.test(prefix); return /[;&|<>()$`\\"']/.test(prefix);
} }
function parseEnvVars(envVarsStr) {
if (!envVarsStr || envVarsStr.trim().length === 0)
return {};
const envObj = {};
const pairs = envVarsStr.trim().split(/\s+/);
for (const pair of pairs) {
const eqIndex = pair.indexOf("=");
if (eqIndex > 0) {
const key = pair.substring(0, eqIndex);
const value = pair.substring(eqIndex + 1);
envObj[key] = value;
}
}
return envObj;
}
function launchDesktopEntry(desktopEntry, useNvidia) { function launchDesktopEntry(desktopEntry, useNvidia) {
let cmd = desktopEntry.command; let cmd = desktopEntry.command;
if (useNvidia && nvidiaCommand) if (useNvidia && nvidiaCommand)
cmd = [nvidiaCommand].concat(cmd); cmd = [nvidiaCommand].concat(cmd);
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
const override = SessionData.getAppOverride(appId);
if (override?.extraFlags) {
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0);
cmd = cmd.concat(extraArgs);
}
const userPrefix = SettingsData.launchPrefix?.trim() || ""; const userPrefix = SettingsData.launchPrefix?.trim() || "";
const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || ""; const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || "";
const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix; const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix;
const workDir = desktopEntry.workingDirectory || Quickshell.env("HOME"); const workDir = desktopEntry.workingDirectory || Quickshell.env("HOME");
const cursorEnv = typeof SettingsData.getCursorEnvironment === "function" ? SettingsData.getCursorEnvironment() : {}; const cursorEnv = typeof SettingsData.getCursorEnvironment === "function" ? SettingsData.getCursorEnvironment() : {};
const overrideEnv = override?.envVars ? parseEnvVars(override.envVars) : {};
const finalEnv = Object.assign({}, cursorEnv, overrideEnv);
if (desktopEntry.runInTerminal) { if (desktopEntry.runInTerminal) {
const terminal = Quickshell.env("TERMINAL") || "xterm"; const terminal = Quickshell.env("TERMINAL") || "xterm";
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" "); const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
@@ -200,7 +227,7 @@ Singleton {
Quickshell.execDetached({ Quickshell.execDetached({
command: [terminal, "-e", "sh", "-c", shellCmd], command: [terminal, "-e", "sh", "-c", shellCmd],
workingDirectory: workDir, workingDirectory: workDir,
environment: cursorEnv environment: finalEnv
}); });
return; return;
} }
@@ -210,7 +237,7 @@ Singleton {
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", `${prefix} ${escapedCmd}`], command: ["sh", "-c", `${prefix} ${escapedCmd}`],
workingDirectory: workDir, workingDirectory: workDir,
environment: cursorEnv environment: finalEnv
}); });
return; return;
} }
@@ -221,7 +248,7 @@ Singleton {
Quickshell.execDetached({ Quickshell.execDetached({
command: cmd, command: cmd,
workingDirectory: workDir, workingDirectory: workDir,
environment: cursorEnv environment: finalEnv
}); });
} }

View File

@@ -29,6 +29,7 @@ Item {
property bool shouldBeVisible: false property bool shouldBeVisible: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool backgroundInteractive: true property bool backgroundInteractive: true
property bool contentHandlesKeys: false
property real storedBarThickness: Theme.barHeight - 4 property real storedBarThickness: Theme.barHeight - 4
property real storedBarSpacing: 4 property real storedBarSpacing: 4
@@ -461,8 +462,12 @@ Item {
id: focusHelper id: focusHelper
parent: contentContainer parent: contentContainer
anchors.fill: parent anchors.fill: parent
focus: true visible: !root.contentHandlesKeys
enabled: !root.contentHandlesKeys
focus: !root.contentHandlesKeys
Keys.onPressed: event => { Keys.onPressed: event => {
if (root.contentHandlesKeys)
return;
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
close(); close();
event.accepted = true; event.accepted = true;