diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 4b2f54af..776958d5 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -399,7 +399,9 @@ "tooltip": "Open calendar" }, "context-menu": { + "activate-app": "Activate {app}", "clear-history": "Clear history", + "close-app": "Close {app}", "cycle-visualizer": "Cycle visualizer", "disable-bluetooth": "Disable Bluetooth", "disable-dnd": "Disable Do Not Disturb", diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml index dd0b5b4c..cf132dc2 100644 --- a/Modules/Bar/Widgets/TaskbarGrouped.qml +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Wayland import Quickshell.Widgets import qs.Commons +import qs.Modules.Bar.Extras import qs.Services.Compositor import qs.Services.UI import qs.Widgets @@ -47,6 +48,10 @@ Item { property int wheelAccumulatedDelta: 0 property bool wheelCooldown: false + // Context menu state + property var selectedWindow: null + property string selectedAppName: "" + function refreshWorkspaces() { localWorkspaces.clear(); if (!screen) @@ -156,6 +161,53 @@ Item { } } + NPopupContextMenu { + id: contextMenu + + model: { + var items = []; + if (selectedWindow) { + items.push({ + "label": I18n.tr("context-menu.activate-app", { + "app": selectedAppName + }), + "action": "activate", + "icon": "focus" + }); + items.push({ + "label": I18n.tr("context-menu.close-app", { + "app": selectedAppName + }), + "action": "close", + "icon": "x" + }); + } + items.push({ + "label": I18n.tr("context-menu.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }); + return items; + } + + onTriggered: action => { + var popupMenuWindow = PanelService.getPopupMenuWindow(screen); + if (popupMenuWindow) { + popupMenuWindow.close(); + } + + if (action === "activate" && selectedWindow) { + CompositorService.focusWindow(selectedWindow); + } else if (action === "close" && selectedWindow) { + CompositorService.closeWindow(selectedWindow); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + selectedWindow = null; + selectedAppName = ""; + } + } + // Debounce timer for wheel interactions Timer { id: wheelDebounce @@ -301,7 +353,15 @@ Item { if (mouse.button === Qt.LeftButton) { CompositorService.focusWindow(model); } else if (mouse.button === Qt.RightButton) { - CompositorService.closeWindow(model); + TooltipService.hide(); + root.selectedWindow = model; + root.selectedAppName = CompositorService.getCleanAppName(model.appId, model.title); + var popupMenuWindow = PanelService.getPopupMenuWindow(screen); + if (popupMenuWindow) { + const pos = BarService.getContextMenuPosition(taskbarItem, contextMenu.implicitWidth, contextMenu.implicitHeight); + contextMenu.openAtItem(taskbarItem, pos.x, pos.y); + popupMenuWindow.showContextMenu(contextMenu); + } } } onEntered: { diff --git a/Services/Compositor/CompositorService.qml b/Services/Compositor/CompositorService.qml index 51add9b3..539c5079 100644 --- a/Services/Compositor/CompositorService.qml +++ b/Services/Compositor/CompositorService.qml @@ -278,6 +278,14 @@ Singleton { return ""; } + // Get clean app name from appId + // Extracts the last segment from reverse domain notation (e.g., "org.kde.dolphin" -> "Dolphin") + // Falls back to title if appId is empty + function getCleanAppName(appId, fallbackTitle) { + var name = (appId || "").split(".").pop() || fallbackTitle || "Unknown"; + return name.charAt(0).toUpperCase() + name.slice(1); + } + function getWindowsForWorkspace(workspaceId) { var windowsInWs = []; for (var i = 0; i < windows.count; i++) {