1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 05:55:37 -05:00

dock: re-work to separate pins from all open windows

This commit is contained in:
bbedward
2025-08-18 14:57:30 -04:00
parent b75342bf93
commit 525ea5ce1c
6 changed files with 506 additions and 613 deletions

View File

@@ -7,307 +7,297 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
property var appData property var appData
property var contextMenu: null property var contextMenu: null
property var windowsMenu: null property var windowsMenu: null
property var dockApps: null property var dockApps: null
property int index: -1 property int index: -1
property bool longPressing: false property bool longPressing: false
property bool dragging: false property bool dragging: false
property point dragStartPos: Qt.point(0, 0) property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0) property point dragOffset: Qt.point(0, 0)
property int targetIndex: -1 property int targetIndex: -1
property int originalIndex: -1 property int originalIndex: -1
property bool showWindowTitle: false
property string windowTitle: ""
property bool isHovered: mouseArea.containsMouse && !dragging
property bool showTooltip: mouseArea.containsMouse && !dragging
property string tooltipText: {
if (!appData)
return "";
width: 40 // For window type, show app name + window title
height: 40 if (appData.type === "window" && showWindowTitle) {
var desktopEntry = DesktopEntries.byId(appData.appId);
property bool isHovered: mouseArea.containsMouse && !dragging var appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId;
return appName + (windowTitle ? " • " + windowTitle : "");
transform: Translate {
id: translateY
y: 0
}
SequentialAnimation {
id: bounceAnimation
running: false
NumberAnimation {
target: translateY
property: "y"
to: -10
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: translateY
property: "y"
to: -8
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
NumberAnimation {
id: exitAnimation
running: false
target: translateY
property: "y"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
onIsHoveredChanged: {
if (isHovered) {
exitAnimation.stop()
if (!bounceAnimation.running) {
bounceAnimation.restart()
}
} else {
bounceAnimation.stop()
exitAnimation.restart()
}
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (appData && appData.isPinned) {
longPressing = true
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.bottomMargin: -20
hoverEnabled: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && appData
&& appData.isPinned) {
dragStartPos = Qt.point(mouse.x, mouse.y)
longPressTimer.start()
}
}
onReleased: mouse => {
longPressTimer.stop()
if (longPressing) {
if (dragging && targetIndex >= 0
&& targetIndex !== originalIndex && dockApps) {
dockApps.movePinnedApp(originalIndex, targetIndex)
}
longPressing = false
dragging = false
dragOffset = Qt.point(0, 0)
targetIndex = -1
originalIndex = -1
}
}
onPositionChanged: mouse => {
if (longPressing && !dragging) {
var distance = Math.sqrt(
Math.pow(mouse.x - dragStartPos.x,
2) + Math.pow(mouse.y - dragStartPos.y,
2))
if (distance > 5) {
dragging = true
targetIndex = index
originalIndex = index
}
}
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x,
mouse.y - dragStartPos.y)
if (dockApps) {
var threshold = 40
var newTargetIndex = targetIndex
if (dragOffset.x > threshold
&& targetIndex < dockApps.pinnedAppCount - 1) {
newTargetIndex = targetIndex + 1
} else if (dragOffset.x < -threshold
&& targetIndex > 0) {
newTargetIndex = targetIndex - 1
}
if (newTargetIndex !== targetIndex) {
targetIndex = newTargetIndex
dragStartPos = Qt.point(mouse.x, mouse.y)
}
}
}
}
onClicked: mouse => {
if (!appData || longPressing)
return
if (mouse.button === Qt.LeftButton) {
var windowCount = appData.windows ? appData.windows.count : 0
if (windowCount === 0) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment
|| ""
})
}
Quickshell.execDetached(["gtk-launch", appData.appId])
}
} else if (windowCount === 1) {
var window = appData.windows.get(0)
NiriService.focusWindow(window.id)
} else {
windowsMenu.showForButton(root, appData, 40)
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment
|| ""
})
}
Quickshell.execDetached(["gtk-launch", appData.appId])
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu) {
contextMenu.showForButton(root, appData, 40)
}
}
}
}
property bool showTooltip: mouseArea.containsMouse && !dragging
property string tooltipText: {
if (!appData || !appData.appId)
return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId
}
IconImage {
id: iconImg
width: 40
height: 40
anchors.centerIn: parent
source: {
if (!appData || !appData.appId)
return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(
desktopEntry.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme)
return iconPath
}
return ""
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
implicitSize: 40
}
Rectangle {
width: 40
height: 40
anchors.centerIn: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId)
return "?"
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
} }
return appData.appId.charAt(0).toUpperCase() // For pinned apps, just show app name
} if (!appData.appId)
font.pixelSize: 14 return "";
color: Theme.primary
font.weight: Font.Bold var desktopEntry = DesktopEntries.byId(appData.appId);
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId;
} }
}
Row { width: 40
anchors.horizontalCenter: parent.horizontalCenter height: 40
anchors.bottom: parent.bottom onIsHoveredChanged: {
anchors.bottomMargin: -2 if (isHovered) {
spacing: 2 exitAnimation.stop();
if (!bounceAnimation.running)
bounceAnimation.restart();
Repeater { } else {
model: appData && appData.windows ? Math.min(appData.windows.count, 4) : 0 bounceAnimation.stop();
exitAnimation.restart();
}
}
Rectangle { SequentialAnimation {
width: appData && appData.windows && appData.windows.count <= 3 ? 5 : 3 id: bounceAnimation
running: false
NumberAnimation {
target: translateY
property: "y"
to: -10
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: translateY
property: "y"
to: -8
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
NumberAnimation {
id: exitAnimation
running: false
target: translateY
property: "y"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (appData && appData.isPinned)
longPressing = true;
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.bottomMargin: -20
hoverEnabled: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (mouse) => {
if (mouse.button === Qt.LeftButton && appData && appData.isPinned) {
dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: (mouse) => {
longPressTimer.stop();
if (longPressing) {
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps)
dockApps.movePinnedApp(originalIndex, targetIndex);
longPressing = false;
dragging = false;
dragOffset = Qt.point(0, 0);
targetIndex = -1;
originalIndex = -1;
}
}
onPositionChanged: (mouse) => {
if (longPressing && !dragging) {
var distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2));
if (distance > 5) {
dragging = true;
targetIndex = index;
originalIndex = index;
}
}
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y);
if (dockApps) {
var threshold = 40;
var newTargetIndex = targetIndex;
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1)
newTargetIndex = targetIndex + 1;
else if (dragOffset.x < -threshold && targetIndex > 0)
newTargetIndex = targetIndex - 1;
if (newTargetIndex !== targetIndex) {
targetIndex = newTargetIndex;
dragStartPos = Qt.point(mouse.x, mouse.y);
}
}
}
}
onClicked: (mouse) => {
if (!appData || longPressing)
return ;
if (mouse.button === Qt.LeftButton) {
// Handle based on type
if (appData.type === "pinned") {
// Launch the pinned app
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId);
if (desktopEntry)
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
});
Quickshell.execDetached(["gtk-launch", appData.appId]);
}
} else if (appData.type === "window") {
// Focus the specific window
if (appData.windowId)
NiriService.focusWindow(appData.windowId);
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId);
if (desktopEntry)
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
});
Quickshell.execDetached(["gtk-launch", appData.appId]);
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu)
contextMenu.showForButton(root, appData, 40);
}
}
}
IconImage {
id: iconImg
width: 40
height: 40
anchors.centerIn: parent
source: {
if (!appData || !appData.appId)
return "";
var desktopEntry = DesktopEntries.byId(appData.appId);
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(desktopEntry.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme);
return iconPath;
}
return "";
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
implicitSize: 40
}
Rectangle {
width: 40
height: 40
anchors.centerIn: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId)
return "?";
var desktopEntry = DesktopEntries.byId(appData.appId);
if (desktopEntry && desktopEntry.name)
return desktopEntry.name.charAt(0).toUpperCase();
return appData.appId.charAt(0).toUpperCase();
}
font.pixelSize: 14
color: Theme.primary
font.weight: Font.Bold
}
}
// Indicator for running/focused state
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: -2
width: 8
height: 2 height: 2
radius: 1 radius: 1
visible: appData && (appData.isRunning || appData.type === "window")
color: { color: {
if (!appData || !appData.windows || appData.windows.count === 0) if (!appData)
return "transparent" return "transparent";
var window = appData.windows.get(index)
return window // For window type, check if focused
&& window.id == NiriService.focusedWindowId ? Theme.primary : Qt.rgba( if (appData.type === "window" && appData.isFocused)
Theme.surfaceText.r, return Theme.primary;
Theme.surfaceText.g,
Theme.surfaceText.b, // For running apps, show dimmer indicator
0.6) if (appData.isRunning || appData.type === "window")
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6);
return "transparent";
} }
}
} }
}
transform: Translate {
id: translateY
y: 0
}
} }

View File

@@ -48,87 +48,65 @@ Item {
clear() clear()
var items = [] var items = []
// Use ordered app IDs if available from Niri, fallback to unordered
var runningApps = NiriService.niriAvailable && NiriService.getRunningAppIdsOrdered
? NiriService.getRunningAppIdsOrdered()
: NiriService.getRunningAppIds()
var pinnedApps = [...(SessionData.pinnedApps || [])] var pinnedApps = [...(SessionData.pinnedApps || [])]
var addedApps = new Set()
// First section: Pinned apps (always visible, not representing running windows)
pinnedApps.forEach(appId => { pinnedApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
var windows = NiriService.getWindowsByAppId(
appId)
items.push({
"appId": appId,
"windows": windows,
"isPinned": true,
"isRunning": windows.length > 0
})
addedApps.add(lowerAppId)
}
})
root.pinnedAppCount = pinnedApps.length
var appUsageRanking = AppUsageHistoryData.appUsageRanking || {}
var unpinnedApps = []
var unpinnedAppsSet = new Set()
// First: Add ALL currently running apps that aren't pinned
// They come pre-ordered from NiriService if Niri is available
runningApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
unpinnedApps.push(appId)
unpinnedAppsSet.add(lowerAppId)
}
})
// Then: Fill remaining slots up to 3 with recently used apps
var remainingSlots = Math.max(0, 3 - unpinnedApps.length)
if (remainingSlots > 0) {
// Sort recent apps by usage
var recentApps = []
for (var appId in appUsageRanking) {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId) && !unpinnedAppsSet.has(
lowerAppId)) {
recentApps.push({
"appId": appId,
"lastUsed": appUsageRanking[appId].lastUsed
|| 0
})
}
}
recentApps.sort((a, b) => b.lastUsed - a.lastUsed)
var recentToAdd = Math.min(remainingSlots, recentApps.length)
for (var i = 0; i < recentToAdd; i++) {
unpinnedApps.push(recentApps[i].appId)
}
}
if (pinnedApps.length > 0 && unpinnedApps.length > 0) {
items.push({ items.push({
"appId": "__SEPARATOR__", "type": "pinned",
"windows": [], "appId": appId,
"isPinned": false, "windowId": -1, // Use -1 instead of null to avoid ListModel warnings
"isRunning": false "windowTitle": "",
}) "workspaceId": -1, // Use -1 instead of null
"isPinned": true,
"isRunning": false,
"isFocused": false
})
})
root.pinnedAppCount = pinnedApps.length
// Add separator between pinned and running if both exist
if (pinnedApps.length > 0 && NiriService.windows.length > 0) {
items.push({
"type": "separator",
"appId": "__SEPARATOR__",
"windowId": -1, // Use -1 instead of null
"windowTitle": "",
"workspaceId": -1, // Use -1 instead of null
"isPinned": false,
"isRunning": false,
"isFocused": false
})
} }
unpinnedApps.forEach(appId => {
var windows = NiriService.getWindowsByAppId( // Second section: Running windows (sorted by display->workspace->position)
appId) // NiriService.windows is already sorted by sortWindowsByLayout
items.push({ NiriService.windows.forEach(window => {
"appId": appId, // Limit window title length for tooltip
"windows": windows, var title = window.title || "(Unnamed)"
"isPinned": false, if (title.length > 50) {
"isRunning": windows.length > 0 title = title.substring(0, 47) + "..."
}) }
})
// Check if this window is focused - compare as numbers
var isFocused = window.id == NiriService.focusedWindowId
items.push({
"type": "window",
"appId": window.app_id || "",
"windowId": window.id || -1,
"windowTitle": title,
"workspaceId": window.workspace_id || -1,
"isPinned": false,
"isRunning": true,
"isFocused": isFocused
})
})
items.forEach(item => { items.forEach(item => {
append(item) append(item)
}) })
} }
} }
@@ -136,11 +114,11 @@ Item {
id: delegateItem id: delegateItem
property alias dockButton: button property alias dockButton: button
width: model.appId === "__SEPARATOR__" ? 16 : 40 width: model.type === "separator" ? 16 : 40
height: 40 height: 40
Rectangle { Rectangle {
visible: model.appId === "__SEPARATOR__" visible: model.type === "separator"
width: 2 width: 2
height: 20 height: 20
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
@@ -150,7 +128,7 @@ Item {
DockAppButton { DockAppButton {
id: button id: button
visible: model.appId !== "__SEPARATOR__" visible: model.type !== "separator"
anchors.centerIn: parent anchors.centerIn: parent
width: 40 width: 40
@@ -161,6 +139,10 @@ Item {
windowsMenu: root.windowsMenu windowsMenu: root.windowsMenu
dockApps: root dockApps: root
index: model.index index: model.index
// Override tooltip for windows to show window title
showWindowTitle: model.type === "window"
windowTitle: model.windowTitle || ""
} }
} }
} }
@@ -174,8 +156,12 @@ Item {
function onWindowOpenedOrChanged() { function onWindowOpenedOrChanged() {
dockModel.updateModel() dockModel.updateModel()
} }
function onFocusedWindowIdChanged() {
dockModel.updateModel()
}
} }
Connections { Connections {
target: SessionData target: SessionData
function onPinnedAppsChanged() { function onPinnedAppsChanged() {

View File

@@ -1,97 +1,108 @@
import Quickshell
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool compactMode: SettingsData.focusedWindowCompactMode property bool compactMode: SettingsData.focusedWindowCompactMode
property int availableWidth: 400 property int availableWidth: 400
readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2 readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2
readonly property int maxNormalWidth: 456 readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288 readonly property int maxCompactWidth: 288
width: compactMode ? Math.min(baseWidth, width: compactMode ? Math.min(baseWidth, maxCompactWidth) : Math.min(baseWidth, maxNormalWidth)
maxCompactWidth) : Math.min(baseWidth, height: 30
maxNormalWidth) radius: Theme.cornerRadius
height: 30 color: {
radius: Theme.cornerRadius if (!NiriService.focusedWindowTitle)
color: { return "transparent";
if (!FocusedWindowService.focusedAppName
&& !FocusedWindowService.focusedWindowTitle)
return "transparent"
const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
baseColor.a * Theme.widgetTransparency) }
} clip: true
clip: true visible: NiriService.niriAvailable && NiriService.focusedWindowTitle
visible: FocusedWindowService.niriAvailable
&& (FocusedWindowService.focusedAppName
|| FocusedWindowService.focusedWindowTitle)
Row { Row {
id: contentRow id: contentRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText { StyledText {
id: appText id: appText
text: {
if (!NiriService.focusedWindowId)
return "";
var window = NiriService.windows.find((w) => {
return w.id == NiriService.focusedWindowId;
});
if (!window || !window.app_id)
return "";
var desktopEntry = DesktopEntries.byId(window.app_id);
return desktopEntry && desktopEntry.name ? desktopEntry.name : window.app_id;
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180)
visible: !compactMode && text.length > 0
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode && appText.text && titleText.text
}
StyledText {
id: titleText
text: NiriService.focusedWindowTitle || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 280 : 250)
visible: text.length > 0
}
text: FocusedWindowService.focusedAppName || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180)
visible: !compactMode && text.length > 0
} }
StyledText { MouseArea {
text: "•" id: mouseArea
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter hoverEnabled: true
visible: !compactMode && appText.text && titleText.text
} }
StyledText { Behavior on color {
id: titleText ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
text: FocusedWindowService.focusedWindowTitle || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 280 : 250)
visible: text.length > 0
} }
}
MouseArea { Behavior on width {
id: mouseArea NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
anchors.fill: parent
hoverEnabled: true
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
} }
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

View File

@@ -8,18 +8,16 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property string section: "left" property string section: "left"
property var parentScreen property var parentScreen
property var hoveredItem: null property var hoveredItem: null
property var topBar: null property var topBar: null
// The visual root for this window // The visual root for this window
property Item windowRoot: (Window.window ? Window.window.contentItem : null) property Item windowRoot: (Window.window ? Window.window.contentItem : null)
readonly property int windowCount: NiriService.windows.length readonly property int windowCount: NiriService.windows.length
readonly property int calculatedWidth: windowCount > 0 ? windowCount * 24 + (windowCount - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0 readonly property int calculatedWidth: windowCount > 0 ? windowCount * 24 + (windowCount - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0
width: calculatedWidth width: calculatedWidth
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -27,135 +25,148 @@ Rectangle {
clip: false clip: false
color: { color: {
if (windowCount === 0) if (windowCount === 0)
return "transparent" return "transparent";
const baseColor = Theme.secondaryHover const baseColor = Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
baseColor.a * Theme.widgetTransparency)
} }
Row { Row {
id: windowRow id: windowRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
Repeater { Repeater {
id: windowRepeater id: windowRepeater
model: NiriService.windows model: NiriService.windows
delegate: Item { delegate: Item {
id: delegateItem id: delegateItem
property bool isFocused: String(modelData.id) === String(FocusedWindowService.focusedWindowId)
property bool isFocused: String(modelData.id) === String(NiriService.focusedWindowId)
property string appId: modelData.app_id || "" property string appId: modelData.app_id || ""
property string windowTitle: modelData.title || "(Unnamed)" property string windowTitle: modelData.title || "(Unnamed)"
property int windowId: modelData.id property int windowId: modelData.id
property string tooltipText: { property string tooltipText: {
var appName = "Unknown" var appName = "Unknown";
if (appId) { if (appId) {
var desktopEntry = DesktopEntries.byId(appId) var desktopEntry = DesktopEntries.byId(appId);
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId;
} }
return appName + (windowTitle ? " • " + windowTitle : "") return appName + (windowTitle ? " • " + windowTitle : "");
} }
width: 24 width: 24
height: 24 height: 24
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isFocused) { if (isFocused)
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
} else { else
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent" return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent";
}
} }
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
// App icon // App icon
IconImage { IconImage {
id: iconImg id: iconImg
anchors.centerIn: parent anchors.centerIn: parent
width: 18 width: 18
height: 18 height: 18
source: { source: {
if (!appId) return "" if (!appId)
var desktopEntry = DesktopEntries.byId(appId) return "";
var desktopEntry = DesktopEntries.byId(appId);
if (desktopEntry && desktopEntry.icon) { if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath( var iconPath = Quickshell.iconPath(desktopEntry.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme);
desktopEntry.icon, return iconPath;
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme)
return iconPath
} }
return "" return "";
} }
smooth: true smooth: true
mipmap: true mipmap: true
asynchronous: true asynchronous: true
visible: status === Image.Ready visible: status === Image.Ready
} }
// Fallback text if no icon found // Fallback text if no icon found
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !iconImg.visible visible: !iconImg.visible
text: { text: {
if (!appId) return "?" if (!appId)
var desktopEntry = DesktopEntries.byId(appId) return "?";
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase() var desktopEntry = DesktopEntries.byId(appId);
} if (desktopEntry && desktopEntry.name)
return appId.charAt(0).toUpperCase() return desktopEntry.name.charAt(0).toUpperCase();
return appId.charAt(0).toUpperCase();
} }
font.pixelSize: 10 font.pixelSize: 10
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
} }
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
NiriService.focusWindow(windowId) NiriService.focusWindow(windowId);
} }
onEntered: { onEntered: {
root.hoveredItem = delegateItem root.hoveredItem = delegateItem;
var globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height) var globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
tooltipLoader.active = true tooltipLoader.active = true;
if (tooltipLoader.item) { if (tooltipLoader.item) {
var tooltipY = Theme.barHeight + SettingsData.topBarSpacing + Theme.spacingXS var tooltipY = Theme.barHeight + SettingsData.topBarSpacing + Theme.spacingXS;
tooltipLoader.item.showTooltip(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen) tooltipLoader.item.showTooltip(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen);
} }
} }
onExited: { onExited: {
if (root.hoveredItem === delegateItem) { if (root.hoveredItem === delegateItem) {
root.hoveredItem = null root.hoveredItem = null;
if (tooltipLoader.item) { if (tooltipLoader.item)
tooltipLoader.item.hideTooltip() tooltipLoader.item.hideTooltip();
}
tooltipLoader.active = false tooltipLoader.active = false;
} }
} }
} }
} }
} }
} }
Loader { Loader {
id: tooltipLoader id: tooltipLoader
active: false active: false
sourceComponent: RunningAppsTooltip { sourceComponent: RunningAppsTooltip {
} }
} }
} }

View File

@@ -1,116 +0,0 @@
pragma Singleton
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property bool niriAvailable: false
property string focusedAppId: ""
property string focusedAppName: ""
property string focusedWindowTitle: ""
property int focusedWindowId: -1
function updateFromNiriData() {
if (!root.niriAvailable) {
clearFocusedWindow()
return
}
let focusedWindow = NiriService.windows.find(w => w.is_focused)
if (focusedWindow) {
root.focusedAppId = focusedWindow.app_id || ""
root.focusedWindowTitle = focusedWindow.title || ""
root.focusedAppName = getDisplayName(focusedWindow.app_id || "")
root.focusedWindowId = parseInt(focusedWindow.id) || -1
} else {
setWorkspaceFallback()
}
}
function clearFocusedWindow() {
root.focusedAppId = ""
root.focusedAppName = ""
root.focusedWindowTitle = ""
root.focusedWindowId = -1
}
function setWorkspaceFallback() {
if (NiriService.focusedWorkspaceIndex >= 0 && NiriService.allWorkspaces.length > 0) {
const workspace = NiriService.allWorkspaces[NiriService.focusedWorkspaceIndex]
if (workspace) {
root.focusedAppId = "niri"
root.focusedAppName = "niri"
if (workspace.name && workspace.name.length > 0) {
root.focusedWindowTitle = workspace.name
} else {
root.focusedWindowTitle = "workspace " + workspace.idx
}
root.focusedWindowId = -1
} else {
clearFocusedWindow()
}
} else {
clearFocusedWindow()
}
}
function getDisplayName(appId) {
if (!appId)
return ""
const desktopEntry = DesktopEntries.byId(appId)
return desktopEntry && desktopEntry.name ? desktopEntry.name : ""
}
Component.onCompleted: {
root.niriAvailable = NiriService.niriAvailable
NiriService.onNiriAvailableChanged.connect(() => {
root.niriAvailable = NiriService.niriAvailable
if (root.niriAvailable)
updateFromNiriData()
})
if (root.niriAvailable)
updateFromNiriData()
}
Connections {
function onFocusedWindowIdChanged() {
const focusedWindowId = NiriService.focusedWindowId
if (!focusedWindowId) {
setWorkspaceFallback()
return
}
const focusedWindow = NiriService.windows.find(
w => w.id == focusedWindowId)
if (focusedWindow) {
root.focusedAppId = focusedWindow.app_id || ""
root.focusedWindowTitle = focusedWindow.title || ""
root.focusedAppName = getDisplayName(focusedWindow.app_id || "")
root.focusedWindowId = parseInt(focusedWindow.id) || -1
} else {
setWorkspaceFallback()
}
}
function onWindowsChanged() {
updateFromNiriData()
}
function onWindowOpenedOrChanged(windowData) {
if (windowData.is_focused) {
root.focusedAppId = windowData.app_id || ""
root.focusedWindowTitle = windowData.title || ""
root.focusedAppName = getDisplayName(windowData.app_id || "")
root.focusedWindowId = parseInt(windowData.id) || -1
}
}
target: NiriService
}
}

View File

@@ -307,6 +307,17 @@ Singleton {
function handleWindowsChanged(data) { function handleWindowsChanged(data) {
windows = sortWindowsByLayout(data.windows) windows = sortWindowsByLayout(data.windows)
// Extract focused window from initial state
var focusedWindow = windows.find(w => w.is_focused)
if (focusedWindow) {
focusedWindowId = String(focusedWindow.id)
focusedWindowIndex = windows.findIndex(w => w.id === focusedWindow.id)
} else {
focusedWindowId = ""
focusedWindowIndex = -1
}
updateFocusedWindow() updateFocusedWindow()
} }