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
Item {
id: root
id: root
property var appData
property var contextMenu: null
property var windowsMenu: null
property var dockApps: null
property int index: -1
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0)
property int targetIndex: -1
property int originalIndex: -1
property var appData
property var contextMenu: null
property var windowsMenu: null
property var dockApps: null
property int index: -1
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0)
property int targetIndex: -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
height: 40
property bool isHovered: mouseArea.containsMouse && !dragging
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()
// For window type, show app name + window title
if (appData.type === "window" && showWindowTitle) {
var desktopEntry = DesktopEntries.byId(appData.appId);
var appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId;
return appName + (windowTitle ? " • " + windowTitle : "");
}
return appData.appId.charAt(0).toUpperCase()
}
font.pixelSize: 14
color: Theme.primary
font.weight: Font.Bold
// For pinned apps, just show app name
if (!appData.appId)
return "";
var desktopEntry = DesktopEntries.byId(appData.appId);
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId;
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: -2
spacing: 2
width: 40
height: 40
onIsHoveredChanged: {
if (isHovered) {
exitAnimation.stop();
if (!bounceAnimation.running)
bounceAnimation.restart();
Repeater {
model: appData && appData.windows ? Math.min(appData.windows.count, 4) : 0
} else {
bounceAnimation.stop();
exitAnimation.restart();
}
}
Rectangle {
width: appData && appData.windows && appData.windows.count <= 3 ? 5 : 3
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
}
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
radius: 1
visible: appData && (appData.isRunning || appData.type === "window")
color: {
if (!appData || !appData.windows || appData.windows.count === 0)
return "transparent"
var window = appData.windows.get(index)
return window
&& window.id == NiriService.focusedWindowId ? Theme.primary : Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.6)
if (!appData)
return "transparent";
// For window type, check if focused
if (appData.type === "window" && appData.isFocused)
return Theme.primary;
// For running apps, show dimmer indicator
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()
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 addedApps = new Set()
// First section: Pinned apps (always visible, not representing running windows)
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({
"appId": "__SEPARATOR__",
"windows": [],
"isPinned": false,
"isRunning": false
})
"type": "pinned",
"appId": appId,
"windowId": -1, // Use -1 instead of null to avoid ListModel warnings
"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(
appId)
items.push({
"appId": appId,
"windows": windows,
"isPinned": false,
"isRunning": windows.length > 0
})
})
// Second section: Running windows (sorted by display->workspace->position)
// NiriService.windows is already sorted by sortWindowsByLayout
NiriService.windows.forEach(window => {
// Limit window title length for tooltip
var title = window.title || "(Unnamed)"
if (title.length > 50) {
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 => {
append(item)
})
append(item)
})
}
}
@@ -136,11 +114,11 @@ Item {
id: delegateItem
property alias dockButton: button
width: model.appId === "__SEPARATOR__" ? 16 : 40
width: model.type === "separator" ? 16 : 40
height: 40
Rectangle {
visible: model.appId === "__SEPARATOR__"
visible: model.type === "separator"
width: 2
height: 20
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
@@ -150,7 +128,7 @@ Item {
DockAppButton {
id: button
visible: model.appId !== "__SEPARATOR__"
visible: model.type !== "separator"
anchors.centerIn: parent
width: 40
@@ -161,6 +139,10 @@ Item {
windowsMenu: root.windowsMenu
dockApps: root
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() {
dockModel.updateModel()
}
function onFocusedWindowIdChanged() {
dockModel.updateModel()
}
}
Connections {
target: SessionData
function onPinnedAppsChanged() {

View File

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