feat(bar): add focused window details popout (#3139)

* feat(bar): add focused window details popout

* fix(bar): address focused window review

* fix(bar): resolve focused window pid by identity

* fix(bar): refine focused window translations
This commit is contained in:
Huỳnh Thiện Lộc
2026-08-21 14:54:58 -04:00
committed by GitHub
parent 124578b695
commit a669253c58
4 changed files with 328 additions and 1 deletions
@@ -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 {}
}
}
@@ -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()
}
}
}
}
}
}
+1
View File
@@ -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 || [],
+2
View File
@@ -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,