diff --git a/quickshell/Modules/DankBar/Widgets/FocusedApp.qml b/quickshell/Modules/DankBar/Widgets/FocusedApp.qml index 9a31d3f27..cd0a989cd 100644 --- a/quickshell/Modules/DankBar/Widgets/FocusedApp.qml +++ b/quickshell/Modules/DankBar/Widgets/FocusedApp.qml @@ -34,6 +34,34 @@ BasePill { property bool isHovered: mouseArea.containsMouse property bool isAutoHideBar: false + function resolveSortedWindow() { + const sortedWindows = CompositorService.sortedToplevels || []; + const exactMatch = sortedWindows.find(window => window === activeWindow || window.wayland === activeWindow || window.sourceToplevel === activeWindow); + if (exactMatch) + return exactMatch; + + const titleMatches = sortedWindows.filter(window => window.appId === activeWindow.appId && window.title === activeWindow.title); + return titleMatches.length === 1 ? titleMatches[0] : null; + } + + function resolveActiveWindowPid() { + if (!activeWindow) + return 0; + if (CompositorService.isNiri) { + const sortedWindow = resolveSortedWindow(); + return sortedWindow?.niriWindowId !== undefined ? NiriService.windows.find(w => w.id === sortedWindow.niriWindowId)?.pid || 0 : 0; + } + if (CompositorService.isHyprland) { + const hyprWindow = Array.from(Hyprland.toplevels?.values || []).find(t => t.wayland === activeWindow); + return hyprWindow?.lastIpcObject?.pid || 0; + } + if (CompositorService.isMango) { + const sortedWindow = resolveSortedWindow(); + return sortedWindow?.mangoWindowId !== undefined ? MangoService.windows.find(w => w.id === sortedWindow.mangoWindowId)?.pid || 0 : 0; + } + return activeWindow.pid || 0; + } + readonly property real minTooltipY: { if (!parentScreen || !isVerticalOrientation) { return 0; @@ -353,7 +381,7 @@ BasePill { id: mouseArea anchors.fill: parent hoverEnabled: root.isVerticalOrientation - acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor onEntered: { if (root.isVerticalOrientation && activeWindow && activeWindow.appId && root.parentScreen) { tooltipLoader.active = true; @@ -378,6 +406,28 @@ BasePill { } tooltipLoader.active = false; } + + acceptedButtons: Qt.LeftButton + onClicked: { + if (!activeWindow || !root.parentScreen) + return; + if (tooltipLoader.item) + tooltipLoader.item.hide(); + tooltipLoader.active = false; + + focusedWindowPopoutLoader.active = true; + if (!focusedWindowPopoutLoader.item) + return; + + const globalPos = root.visualContent.mapToItem(null, 0, 0); + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + const position = SettingsData.getPopupTriggerPosition(globalPos, root.parentScreen, root.barThickness, root.visualWidth, root.barSpacing, barPosition, root.barConfig); + const popout = focusedWindowPopoutLoader.item; + popout.currentWindow = activeWindow; + popout.processId = root.resolveActiveWindowPid(); + popout.setTriggerPosition(position.x, position.y, position.width, root.section, root.parentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig); + popout.toggle(); + } } Loader { @@ -385,4 +435,10 @@ BasePill { active: false sourceComponent: DankTooltip {} } + + Loader { + id: focusedWindowPopoutLoader + active: false + sourceComponent: FocusedWindowContextMenu {} + } } diff --git a/quickshell/Modules/DankBar/Widgets/FocusedWindowContextMenu.qml b/quickshell/Modules/DankBar/Widgets/FocusedWindowContextMenu.qml new file mode 100644 index 000000000..8f3f885f6 --- /dev/null +++ b/quickshell/Modules/DankBar/Widgets/FocusedWindowContextMenu.qml @@ -0,0 +1,268 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +DankPopout { + id: root + + property var currentWindow: null + property int processId: 0 + + readonly property string appId: currentWindow?.appId || "" + readonly property string windowTitle: currentWindow?.title || "" + readonly property string appName: appId ? Paths.getAppName(appId, DesktopEntries.heuristicLookup(Paths.moddedAppId(appId))) : I18n.tr("Unknown") + readonly property int pid: processId + + layerNamespace: "dms:focused-window-popout" + popupWidth: 340 + popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 260 + triggerWidth: 40 + positioning: "" + shouldBeVisible: false + + function copyValue(value) { + if (!value) + return; + Quickshell.execDetached(["dms", "cl", "copy", value.toString()]); + ToastService.showInfo(I18n.tr("Copied to clipboard")); + close(); + } + + function killWindow() { + if (pid > 0) + Quickshell.execDetached(["kill", pid.toString()]); + close(); + } + + function addWindowRule() { + if (!currentWindow || !PopoutService.windowRuleModalLoader) + return; + close(); + PopoutService.windowRuleModalLoader.active = true; + Qt.callLater(() => { + if (PopoutService.windowRuleModalLoader.item) + PopoutService.windowRuleModalLoader.item.show(currentWindow); + }); + } + + onBackgroundClicked: close() + + content: Component { + Rectangle { + id: contentRoot + + implicitWidth: 340 + implicitHeight: contentColumn.implicitHeight + Theme.spacingS * 2 + anchors.fill: parent + color: "transparent" + focus: true + + Keys.onEscapePressed: event => { + root.close(); + event.accepted = true; + } + + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingXXS + + Row { + width: parent.width + height: 28 + spacing: Theme.spacingS + + IconImage { + width: 20 + height: 20 + source: Paths.getAppIcon(root.appId, DesktopEntries.heuristicLookup(root.appId)) + asynchronous: true + mipmap: true + smooth: true + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + width: parent.width - 20 - Theme.spacingS + text: root.appName + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.Wrap + clip: true + anchors.verticalCenter: parent.verticalCenter + } + } + + Repeater { + model: [ + { label: I18n.tr("App ID"), value: root.appId, copyable: !!root.appId }, + { label: I18n.tr("Title"), value: root.windowTitle || I18n.tr("Untitled"), copyable: !!root.windowTitle }, + { label: I18n.tr("PID", "Label for the process ID row in the focused window popout"), value: root.pid > 0 ? root.pid.toString() : I18n.tr("Unavailable"), copyable: root.pid > 0 } + ] + + delegate: Rectangle { + required property var modelData + + width: contentColumn.width + height: 40 + radius: Theme.cornerRadius + color: copyArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(BlurService.hoverColor(Theme.widgetBaseHoverColor), 0) + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingXS + spacing: Theme.spacingS + + StyledText { + width: 72 + text: modelData.label + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + width: parent.width - 72 - Theme.spacingS + text: modelData.value + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideMiddle + clip: true + anchors.verticalCenter: parent.verticalCenter + } + } + + DankRipple { + id: copyRipple + rippleColor: Theme.surfaceText + cornerRadius: parent.radius + } + + MouseArea { + id: copyArea + anchors.fill: parent + hoverEnabled: true + enabled: modelData.copyable + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onPressed: mouse => copyRipple.trigger(mouse.x, mouse.y) + onClicked: root.copyValue(modelData.value) + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineVariant + } + + Item { + visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango + width: parent.width + height: visible ? 32 : 0 + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: ruleArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(BlurService.hoverColor(Theme.widgetBaseHoverColor), 0) + } + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "rule" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Add Window Rule") + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeSmall + anchors.verticalCenter: parent.verticalCenter + } + } + + DankRipple { + id: ruleRipple + anchors.fill: parent + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: ruleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => ruleRipple.trigger(mouse.x, mouse.y) + onClicked: root.addWindowRule() + } + } + + Item { + width: parent.width + height: 32 + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: killArea.containsMouse ? Theme.errorHover : Theme.withAlpha(Theme.errorHover, 0) + } + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "close" + size: 16 + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Kill Process") + color: Theme.error + font.pixelSize: Theme.fontSizeSmall + anchors.verticalCenter: parent.verticalCenter + } + } + + DankRipple { + id: killRipple + anchors.fill: parent + rippleColor: Theme.error + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: killArea + anchors.fill: parent + hoverEnabled: true + enabled: root.pid > 0 + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onPressed: mouse => killRipple.trigger(mouse.x, mouse.y) + onClicked: root.killWindow() + } + } + } + } + } +} diff --git a/quickshell/Services/MangoService.qml b/quickshell/Services/MangoService.qml index e544ed1ac..3ee505ddd 100644 --- a/quickshell/Services/MangoService.qml +++ b/quickshell/Services/MangoService.qml @@ -369,6 +369,7 @@ Singleton { const enriched = { "appId": bestMatch.appId, "title": bestMatch.title, + "sourceToplevel": bestMatch, "activated": !!client.is_focused, "mangoWindowId": client.id, "mangoTags": client.tags || [], diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index df7ccbea8..1006596a8 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -1012,6 +1012,7 @@ Singleton { const enrichedToplevel = { "appId": bestMatch.appId, "title": bestMatch.title, + "sourceToplevel": bestMatch, "activated": isFocused, "niriWindowId": niriWindow.id, "niriWorkspaceId": niriWindow.workspace_id, @@ -1085,6 +1086,7 @@ Singleton { const enrichedToplevel = { "appId": bestMatch.appId, "title": bestMatch.title, + "sourceToplevel": bestMatch, "activated": isFocused, "niriWindowId": niriWindow.id, "niriWorkspaceId": niriWindow.workspace_id,