mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
compositor service & use toplevels instead of niri data
This commit is contained in:
@@ -381,7 +381,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function hasNamedWorkspaces() {
|
||||
if (typeof NiriService === "undefined" || !NiriService.niriAvailable)
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
|
||||
return false
|
||||
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
@@ -394,7 +394,7 @@ Singleton {
|
||||
|
||||
function getNamedWorkspaces() {
|
||||
var namedWorkspaces = []
|
||||
if (typeof NiriService === "undefined" || !NiriService.niriAvailable)
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
|
||||
return namedWorkspaces
|
||||
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
|
||||
@@ -625,6 +625,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Component.onCompleted: {
|
||||
if (typeof Colors !== "undefined")
|
||||
Colors.colorsUpdated.connect(root.onColorsUpdated)
|
||||
|
||||
@@ -16,24 +16,18 @@ PanelWindow {
|
||||
|
||||
property var modelData
|
||||
property var contextMenu
|
||||
property var windowsMenu
|
||||
property bool autoHide: SettingsData.dockAutoHide
|
||||
property real backgroundTransparency: SettingsData.dockTransparency
|
||||
|
||||
property bool contextMenuOpen: (contextMenu && contextMenu.visible
|
||||
&& contextMenu.screen === modelData)
|
||||
|| (windowsMenu && windowsMenu.visible
|
||||
&& windowsMenu.screen === modelData)
|
||||
property bool windowIsFullscreen: {
|
||||
if (!NiriService.focusedWindowId || !NiriService.niriAvailable)
|
||||
return false
|
||||
var focusedWindow = NiriService.windows.find(
|
||||
w => w.id === NiriService.focusedWindowId)
|
||||
if (!focusedWindow)
|
||||
if (!ToplevelManager.activeToplevel)
|
||||
return false
|
||||
var activeWindow = ToplevelManager.activeToplevel
|
||||
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
|
||||
return fullscreenApps.some(app => focusedWindow.app_id
|
||||
&& focusedWindow.app_id.toLowerCase(
|
||||
return fullscreenApps.some(app => activeWindow.appId
|
||||
&& activeWindow.appId.toLowerCase(
|
||||
).includes(app))
|
||||
}
|
||||
property bool reveal: (!autoHide || dockMouseArea.containsMouse
|
||||
@@ -142,7 +136,6 @@ PanelWindow {
|
||||
anchors.bottomMargin: 4
|
||||
|
||||
contextMenu: dock.contextMenu
|
||||
windowsMenu: dock.windowsMenu
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Item {
|
||||
|
||||
property var appData
|
||||
property var contextMenu: null
|
||||
property var windowsMenu: null
|
||||
property var dockApps: null
|
||||
property int index: -1
|
||||
property bool longPressing: false
|
||||
@@ -203,9 +202,10 @@ Item {
|
||||
["gtk-launch", appData.appId])
|
||||
}
|
||||
} else if (appData.type === "window") {
|
||||
// Focus the specific window
|
||||
if (appData.windowId)
|
||||
NiriService.focusWindow(appData.windowId)
|
||||
// Focus the specific window using toplevel
|
||||
if (appData.toplevelObject) {
|
||||
appData.toplevelObject.activate()
|
||||
}
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
if (appData && appData.appId) {
|
||||
@@ -301,8 +301,8 @@ Item {
|
||||
if (!appData)
|
||||
return "transparent"
|
||||
|
||||
// For window type, check if focused
|
||||
if (appData.type === "window" && appData.isFocused)
|
||||
// For window type, check if focused using reactive property
|
||||
if (appData.type === "window" && appData.toplevelObject && appData.toplevelObject.activated)
|
||||
return Theme.primary
|
||||
|
||||
// For running apps, show dimmer indicator
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -9,7 +10,6 @@ Item {
|
||||
id: root
|
||||
|
||||
property var contextMenu: null
|
||||
property var windowsMenu: null
|
||||
property bool requestDockShow: false
|
||||
property int pinnedAppCount: 0
|
||||
|
||||
@@ -62,15 +62,17 @@ Item {
|
||||
"isPinned"// Use -1 instead of null
|
||||
: true,
|
||||
"isRunning": false,
|
||||
"isFocused": false
|
||||
})
|
||||
})
|
||||
|
||||
root.pinnedAppCount = pinnedApps.length
|
||||
|
||||
// Get sorted toplevels from CompositorService
|
||||
var sortedToplevels = CompositorService.sortedToplevels
|
||||
|
||||
// Add separator between pinned and running if both exist
|
||||
if (pinnedApps.length > 0
|
||||
&& NiriService.windows.length > 0) {
|
||||
&& sortedToplevels.length > 0) {
|
||||
items.push({
|
||||
"type": "separator",
|
||||
"appId": "__SEPARATOR__",
|
||||
@@ -85,33 +87,25 @@ Item {
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
})
|
||||
// Second section: Running windows (sorted using Theme.sortToplevels)
|
||||
sortedToplevels.forEach(toplevel => {
|
||||
// Limit window title length for tooltip
|
||||
var title = toplevel.title || "(Unnamed)"
|
||||
if (title.length > 50) {
|
||||
title = title.substring(0, 47) + "..."
|
||||
}
|
||||
|
||||
items.push({
|
||||
"type": "window",
|
||||
"appId": toplevel.appId || "",
|
||||
"windowId": -1, // Toplevel doesn't have numeric ID
|
||||
"windowTitle": title,
|
||||
"workspaceId": -1, // Will be handled by sorting
|
||||
"isPinned": false,
|
||||
"isRunning": true,
|
||||
"toplevelObject": toplevel
|
||||
})
|
||||
})
|
||||
|
||||
items.forEach(item => {
|
||||
append(item)
|
||||
@@ -146,7 +140,6 @@ Item {
|
||||
|
||||
appData: model
|
||||
contextMenu: root.contextMenu
|
||||
windowsMenu: root.windowsMenu
|
||||
dockApps: root
|
||||
index: model.index
|
||||
|
||||
@@ -159,17 +152,12 @@ Item {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onWindowsChanged() {
|
||||
dockModel.updateModel()
|
||||
}
|
||||
function onWindowOpenedOrChanged() {
|
||||
dockModel.updateModel()
|
||||
}
|
||||
function onFocusedWindowIdChanged() {
|
||||
target: CompositorService
|
||||
function onSortedToplevelsChanged() {
|
||||
dockModel.updateModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
|
||||
@@ -187,59 +187,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !!(root.appData && root.appData.windows
|
||||
&& root.appData.windows.count > 0)
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.appData
|
||||
&& root.appData.windows ? root.appData.windows : null
|
||||
|
||||
Rectangle {
|
||||
required property var model
|
||||
width: menuColumn.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: windowArea.containsMouse ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: model.title || "Untitled Window"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: windowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
NiriService.focusWindow(model.id)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !!(root.appData && root.appData.windows
|
||||
&& root.appData.windows.count > 1)
|
||||
visible: root.appData && root.appData.type === "window"
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
@@ -247,16 +195,23 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !!(root.appData && root.appData.windows
|
||||
&& root.appData.windows.count > 1)
|
||||
visible: root.appData && root.appData.type === "window"
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.appData && root.appData.type === "window"
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: closeAllArea.containsMouse ? Qt.rgba(
|
||||
Theme.error.r,
|
||||
Theme.error.g,
|
||||
Theme.error.b,
|
||||
0.12) : "transparent"
|
||||
color: closeArea.containsMouse ? Qt.rgba(
|
||||
Theme.error.r,
|
||||
Theme.error.g,
|
||||
Theme.error.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
@@ -264,25 +219,22 @@ PanelWindow {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Close All Windows"
|
||||
text: "Close Window"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: closeAllArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeAllArea
|
||||
id: closeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!root.appData || !root.appData.windows)
|
||||
return
|
||||
for (var i = 0; i < root.appData.windows.count; i++) {
|
||||
var window = root.appData.windows.get(i)
|
||||
NiriService.closeWindow(window.id)
|
||||
if (root.appData && root.appData.toplevelObject) {
|
||||
root.appData.toplevelObject.close()
|
||||
}
|
||||
root.close()
|
||||
}
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool showWindowsMenu: false
|
||||
property var appData: null
|
||||
property var anchorItem: null
|
||||
property real dockVisibleHeight: 40
|
||||
property int margin: 10
|
||||
|
||||
function showForButton(button, data, dockHeight) {
|
||||
anchorItem = button
|
||||
appData = data
|
||||
dockVisibleHeight = dockHeight || 40
|
||||
|
||||
var dockWindow = button.Window.window
|
||||
if (dockWindow) {
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
var s = Quickshell.screens[i]
|
||||
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
|
||||
root.screen = s
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showWindowsMenu = true
|
||||
}
|
||||
|
||||
function close() {
|
||||
showWindowsMenu = false
|
||||
}
|
||||
|
||||
screen: Quickshell.screens[0]
|
||||
|
||||
visible: showWindowsMenu
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
|
||||
|
||||
onAnchorItemChanged: updatePosition()
|
||||
onVisibleChanged: if (visible)
|
||||
updatePosition()
|
||||
|
||||
function updatePosition() {
|
||||
if (!anchorItem) {
|
||||
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
|
||||
return
|
||||
}
|
||||
|
||||
var dockWindow = anchorItem.Window.window
|
||||
if (!dockWindow) {
|
||||
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
|
||||
return
|
||||
}
|
||||
|
||||
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
|
||||
|
||||
var actualDockHeight = root.dockVisibleHeight // fallback
|
||||
|
||||
function findDockBackground(item) {
|
||||
if (item.objectName === "dockBackground") {
|
||||
return item
|
||||
}
|
||||
for (var i = 0; i < item.children.length; i++) {
|
||||
var found = findDockBackground(item.children[i])
|
||||
if (found)
|
||||
return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
var dockBackground = findDockBackground(dockWindow.contentItem)
|
||||
if (dockBackground) {
|
||||
actualDockHeight = dockBackground.height
|
||||
}
|
||||
|
||||
var dockBottomMargin = 16 // The dock has bottom margin
|
||||
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
|
||||
|
||||
var dockContentWidth = dockWindow.width
|
||||
var screenWidth = root.screen.width
|
||||
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
|
||||
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
|
||||
|
||||
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
|
||||
width: Math.min(600, Math.max(
|
||||
250,
|
||||
windowColumn.implicitWidth + Theme.spacingS * 2))
|
||||
height: Math.max(60, windowColumn.implicitHeight + Theme.spacingS * 2)
|
||||
|
||||
x: {
|
||||
var left = 10
|
||||
var right = root.width - width - 10
|
||||
var want = root.anchorPos.x - width / 2
|
||||
return Math.max(left, Math.min(right, want))
|
||||
}
|
||||
y: Math.max(10, root.anchorPos.y - height + 30)
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
opacity: showWindowsMenu ? 1 : 0
|
||||
scale: showWindowsMenu ? 1 : 0.85
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: windowColumn
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: root.appData
|
||||
&& root.appData.windows ? root.appData.windows : null
|
||||
|
||||
Rectangle {
|
||||
required property var model
|
||||
width: windowColumn.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: windowArea.containsMouse ? Qt.rgba(
|
||||
Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: model.title || "Untitled Window"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: model.is_focused ? Theme.primary : Theme.surfaceText
|
||||
font.weight: model.is_focused ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: windowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
NiriService.focusWindow(model.id)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
hoverEnabled: false
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -12,6 +13,7 @@ Rectangle {
|
||||
readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2
|
||||
readonly property int maxNormalWidth: 456
|
||||
readonly property int maxCompactWidth: 288
|
||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
||||
|
||||
width: compactMode ? Math.min(baseWidth,
|
||||
maxCompactWidth) : Math.min(baseWidth,
|
||||
@@ -19,7 +21,7 @@ Rectangle {
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (!NiriService.focusedWindowTitle)
|
||||
if (!activeWindow || !activeWindow.title)
|
||||
return "transparent"
|
||||
|
||||
const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover
|
||||
@@ -27,7 +29,7 @@ Rectangle {
|
||||
baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
clip: true
|
||||
visible: NiriService.niriAvailable && NiriService.focusedWindowTitle
|
||||
visible: activeWindow && activeWindow.title
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
@@ -39,18 +41,12 @@ Rectangle {
|
||||
id: appText
|
||||
|
||||
text: {
|
||||
if (!NiriService.focusedWindowId)
|
||||
if (!activeWindow || !activeWindow.appId)
|
||||
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)
|
||||
var desktopEntry = DesktopEntries.byId(activeWindow.appId)
|
||||
return desktopEntry
|
||||
&& desktopEntry.name ? desktopEntry.name : window.app_id
|
||||
&& desktopEntry.name ? desktopEntry.name : activeWindow.appId
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
@@ -74,7 +70,7 @@ Rectangle {
|
||||
id: titleText
|
||||
|
||||
text: {
|
||||
var title = NiriService.focusedWindowTitle || ""
|
||||
var title = activeWindow && activeWindow.title ? activeWindow.title : ""
|
||||
var appName = appText.text
|
||||
|
||||
if (!title || !appName)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -15,7 +16,8 @@ Rectangle {
|
||||
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 var sortedToplevels: CompositorService.sortedToplevels
|
||||
readonly property int windowCount: sortedToplevels.length
|
||||
readonly property int calculatedWidth: {
|
||||
if (windowCount === 0)
|
||||
return 0
|
||||
@@ -50,16 +52,15 @@ Rectangle {
|
||||
Repeater {
|
||||
id: windowRepeater
|
||||
|
||||
model: NiriService.windows
|
||||
model: sortedToplevels
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
|
||||
property bool isFocused: String(modelData.id) === String(
|
||||
NiriService.focusedWindowId)
|
||||
property string appId: modelData.app_id || ""
|
||||
property bool isFocused: modelData.activated
|
||||
property string appId: modelData.appId || ""
|
||||
property string windowTitle: modelData.title || "(Unnamed)"
|
||||
property int windowId: modelData.id
|
||||
property var toplevelObject: modelData
|
||||
property string tooltipText: {
|
||||
var appName = "Unknown"
|
||||
if (appId) {
|
||||
@@ -176,7 +177,9 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
NiriService.focusWindow(windowId)
|
||||
if (toplevelObject) {
|
||||
toplevelObject.activate()
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
root.hoveredItem = delegateItem
|
||||
|
||||
@@ -24,7 +24,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
function getDisplayWorkspaces() {
|
||||
if (!NiriService.niriAvailable
|
||||
if (!CompositorService.isNiri
|
||||
|| NiriService.allWorkspaces.length === 0)
|
||||
return [1, 2]
|
||||
|
||||
@@ -41,7 +41,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
function getDisplayActiveWorkspace() {
|
||||
if (!NiriService.niriAvailable
|
||||
if (!CompositorService.isNiri
|
||||
|| NiriService.allWorkspaces.length === 0)
|
||||
return 1
|
||||
|
||||
@@ -68,7 +68,7 @@ Rectangle {
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
|
||||
baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
visible: NiriService.niriAvailable
|
||||
visible: CompositorService.isNiri
|
||||
|
||||
Connections {
|
||||
function onAllWorkspacesChanged() {
|
||||
@@ -83,16 +83,10 @@ Rectangle {
|
||||
root.currentWorkspace = root.getDisplayActiveWorkspace()
|
||||
}
|
||||
|
||||
function onNiriAvailableChanged() {
|
||||
if (NiriService.niriAvailable) {
|
||||
root.workspaceList = SettingsData.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces()
|
||||
root.currentWorkspace = root.getDisplayActiveWorkspace()
|
||||
}
|
||||
}
|
||||
|
||||
target: NiriService
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
function onShowWorkspacePaddingChanged() {
|
||||
var baseList = root.getDisplayWorkspaces()
|
||||
@@ -118,7 +112,7 @@ Rectangle {
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
property int sequentialNumber: index + 1
|
||||
property var workspaceData: {
|
||||
if (isPlaceholder || !NiriService.niriAvailable)
|
||||
if (isPlaceholder || !CompositorService.isNiri)
|
||||
return null
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
var ws = NiriService.allWorkspaces[i]
|
||||
|
||||
84
Services/CompositorService.qml
Normal file
84
Services/CompositorService.qml
Normal file
@@ -0,0 +1,84 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Compositor detection
|
||||
property bool isHyprland: false
|
||||
property bool isNiri: false
|
||||
property string compositor: "unknown"
|
||||
|
||||
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
|
||||
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
|
||||
|
||||
property bool useNiriSorting: isNiri && NiriService
|
||||
|
||||
// Unified sorted toplevels - automatically chooses sorting based on compositor
|
||||
property var sortedToplevels: {
|
||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Only use niri sorting when both compositor is niri AND niri service is ready
|
||||
if (useNiriSorting) {
|
||||
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
|
||||
}
|
||||
|
||||
// For non-niri compositors or when niri isn't ready yet, return unsorted toplevels
|
||||
return ToplevelManager.toplevels.values
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("CompositorService: Starting detection...")
|
||||
detectCompositor()
|
||||
}
|
||||
|
||||
function detectCompositor() {
|
||||
// Check for Hyprland first
|
||||
if (hyprlandSignature && hyprlandSignature.length > 0) {
|
||||
isHyprland = true
|
||||
isNiri = false
|
||||
compositor = "hyprland"
|
||||
console.log("CompositorService: Detected Hyprland with signature:", hyprlandSignature)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for Niri
|
||||
if (niriSocket && niriSocket.length > 0) {
|
||||
// Verify the socket actually exists
|
||||
niriSocketCheck.running = true
|
||||
} else {
|
||||
// No compositor detected, default to Niri
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
compositor = "unknown"
|
||||
console.warn("CompositorService: No compositor detected")
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: niriSocketCheck
|
||||
command: ["test", "-S", root.niriSocket]
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
root.isNiri = true
|
||||
root.isHyprland = false
|
||||
root.compositor = "niri"
|
||||
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
|
||||
} else {
|
||||
root.isHyprland = false
|
||||
root.isNiri = true
|
||||
root.compositor = "niri"
|
||||
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pragma ComponentBehavior
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -22,45 +23,19 @@ Singleton {
|
||||
|
||||
// Window management
|
||||
property var windows: []
|
||||
property int focusedWindowIndex: -1
|
||||
property string focusedWindowTitle: "(No active window)"
|
||||
property string focusedWindowId: ""
|
||||
|
||||
// Overview state
|
||||
property bool inOverview: false
|
||||
|
||||
// Config validation
|
||||
// Internal state (not exposed to external components)
|
||||
property string configValidationOutput: ""
|
||||
property bool hasInitialConnection: false
|
||||
|
||||
signal windowOpenedOrChanged(var windowData)
|
||||
|
||||
// Feature availability
|
||||
property bool niriAvailable: false
|
||||
|
||||
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
|
||||
|
||||
Component.onCompleted: checkNiriAvailability()
|
||||
|
||||
Process {
|
||||
id: niriCheck
|
||||
command: ["test", "-S", root.socketPath]
|
||||
|
||||
onExited: exitCode => {
|
||||
root.niriAvailable = exitCode === 0
|
||||
if (root.niriAvailable) {
|
||||
eventStreamSocket.connected = true
|
||||
fetchOutputs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkNiriAvailability() {
|
||||
niriCheck.running = true
|
||||
}
|
||||
|
||||
function fetchOutputs() {
|
||||
if (niriAvailable) {
|
||||
if (CompositorService.isNiri) {
|
||||
outputsProcess.running = true
|
||||
}
|
||||
}
|
||||
@@ -98,7 +73,7 @@ Singleton {
|
||||
Socket {
|
||||
id: eventStreamSocket
|
||||
path: root.socketPath
|
||||
connected: false
|
||||
connected: CompositorService.isNiri
|
||||
|
||||
onConnectionStateChanged: {
|
||||
if (connected) {
|
||||
@@ -121,7 +96,7 @@ Singleton {
|
||||
Socket {
|
||||
id: requestSocket
|
||||
path: root.socketPath
|
||||
connected: root.niriAvailable
|
||||
connected: CompositorService.isNiri
|
||||
}
|
||||
|
||||
function sortWindowsByLayout(windowList) {
|
||||
@@ -203,8 +178,6 @@ Singleton {
|
||||
handleWindowsChanged(event.WindowsChanged)
|
||||
} else if (event.WindowClosed) {
|
||||
handleWindowClosed(event.WindowClosed)
|
||||
} else if (event.WindowFocusChanged) {
|
||||
handleWindowFocusChanged(event.WindowFocusChanged)
|
||||
} else if (event.WindowOpenedOrChanged) {
|
||||
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
|
||||
} else if (event.WindowLayoutsChanged) {
|
||||
@@ -279,10 +252,6 @@ Singleton {
|
||||
// This is crucial for handling floating window close scenarios
|
||||
if (data.active_window_id !== null
|
||||
&& data.active_window_id !== undefined) {
|
||||
focusedWindowId = String(data.active_window_id)
|
||||
focusedWindowIndex = windows.findIndex(
|
||||
w => w.id == data.active_window_id)
|
||||
|
||||
// Create new windows array with updated focus states to trigger property change
|
||||
let updatedWindows = []
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
@@ -295,13 +264,8 @@ Singleton {
|
||||
updatedWindows.push(updatedWindow)
|
||||
}
|
||||
windows = updatedWindows
|
||||
|
||||
updateFocusedWindow()
|
||||
} else {
|
||||
// No active window in this workspace
|
||||
focusedWindowId = ""
|
||||
focusedWindowIndex = -1
|
||||
|
||||
// Create new windows array with cleared focus states for this workspace
|
||||
let updatedWindows = []
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
@@ -315,42 +279,15 @@ Singleton {
|
||||
updatedWindows.push(updatedWindow)
|
||||
}
|
||||
windows = updatedWindows
|
||||
|
||||
updateFocusedWindow()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
function handleWindowClosed(data) {
|
||||
windows = windows.filter(w => w.id !== data.id)
|
||||
updateFocusedWindow()
|
||||
}
|
||||
|
||||
function handleWindowFocusChanged(data) {
|
||||
if (data.id) {
|
||||
focusedWindowId = data.id
|
||||
focusedWindowIndex = windows.findIndex(w => w.id === data.id)
|
||||
} else {
|
||||
focusedWindowId = ""
|
||||
focusedWindowIndex = -1
|
||||
}
|
||||
updateFocusedWindow()
|
||||
}
|
||||
|
||||
function handleWindowOpenedOrChanged(data) {
|
||||
@@ -368,14 +305,6 @@ Singleton {
|
||||
windows = sortWindowsByLayout([...windows, window])
|
||||
}
|
||||
|
||||
if (window.is_focused) {
|
||||
focusedWindowId = window.id
|
||||
focusedWindowIndex = windows.findIndex(w => w.id === window.id)
|
||||
}
|
||||
|
||||
updateFocusedWindow()
|
||||
|
||||
windowOpenedOrChanged(window)
|
||||
}
|
||||
|
||||
function handleWindowLayoutsChanged(data) {
|
||||
@@ -477,17 +406,8 @@ Singleton {
|
||||
currentOutputWorkspaces = outputWs
|
||||
}
|
||||
|
||||
function updateFocusedWindow() {
|
||||
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
||||
var focusedWin = windows[focusedWindowIndex]
|
||||
focusedWindowTitle = focusedWin.title || "(Unnamed window)"
|
||||
} else {
|
||||
focusedWindowTitle = "(No active window)"
|
||||
}
|
||||
}
|
||||
|
||||
function send(request) {
|
||||
if (!niriAvailable || !requestSocket.connected)
|
||||
if (!CompositorService.isNiri || !requestSocket.connected)
|
||||
return false
|
||||
requestSocket.write(JSON.stringify(request) + "\n")
|
||||
return true
|
||||
@@ -518,25 +438,7 @@ Singleton {
|
||||
return 1
|
||||
}
|
||||
|
||||
function focusWindow(windowId) {
|
||||
return send({
|
||||
"Action": {
|
||||
"FocusWindow": {
|
||||
"id": windowId
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeWindow(windowId) {
|
||||
return send({
|
||||
"Action": {
|
||||
"CloseWindow": {
|
||||
"id": windowId
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function quit() {
|
||||
return send({
|
||||
@@ -548,58 +450,66 @@ Singleton {
|
||||
})
|
||||
}
|
||||
|
||||
function getWindowsByAppId(appId) {
|
||||
if (!appId)
|
||||
return []
|
||||
return windows.filter(w => w.app_id && w.app_id.toLowerCase(
|
||||
) === appId.toLowerCase())
|
||||
|
||||
|
||||
function sortToplevels(toplevels) {
|
||||
if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) {
|
||||
return [...toplevels]
|
||||
}
|
||||
|
||||
// Create a map to match toplevels to niri windows
|
||||
// We'll match by appId and title since toplevels don't have numeric IDs
|
||||
var toplevelToNiriMap = {}
|
||||
|
||||
for (var i = 0; i < toplevels.length; i++) {
|
||||
var toplevel = toplevels[i]
|
||||
if (!toplevel.appId) continue
|
||||
|
||||
// Find matching niri window by appId and optionally title
|
||||
for (var j = 0; j < windows.length; j++) {
|
||||
var niriWindow = windows[j]
|
||||
|
||||
// Match by appId
|
||||
if (niriWindow.app_id === toplevel.appId) {
|
||||
// If title also matches or niri window has no title, use this match
|
||||
if (!niriWindow.title || niriWindow.title === toplevel.title) {
|
||||
toplevelToNiriMap[i] = {
|
||||
niriIndex: j,
|
||||
niriWindow: niriWindow
|
||||
}
|
||||
break
|
||||
}
|
||||
// If we found appId match but no title match yet, store as fallback
|
||||
if (!(i in toplevelToNiriMap)) {
|
||||
toplevelToNiriMap[i] = {
|
||||
niriIndex: j,
|
||||
niriWindow: niriWindow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort toplevels using niri's ordering
|
||||
return [...toplevels].sort((a, b) => {
|
||||
var aIndex = toplevels.indexOf(a)
|
||||
var bIndex = toplevels.indexOf(b)
|
||||
|
||||
var aNiri = toplevelToNiriMap[aIndex]
|
||||
var bNiri = toplevelToNiriMap[bIndex]
|
||||
|
||||
// If both have niri data, use niri ordering
|
||||
if (aNiri && bNiri) {
|
||||
return aNiri.niriIndex - bNiri.niriIndex
|
||||
}
|
||||
|
||||
// If only one has niri data, prioritize it
|
||||
if (aNiri && !bNiri) return -1
|
||||
if (!aNiri && bNiri) return 1
|
||||
|
||||
// If neither has niri data, keep original toplevel order
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
function getRunningAppIds() {
|
||||
var appIds = new Set()
|
||||
windows.forEach(w => {
|
||||
if (w.app_id) {
|
||||
appIds.add(w.app_id.toLowerCase())
|
||||
}
|
||||
})
|
||||
return Array.from(appIds)
|
||||
}
|
||||
|
||||
function getRunningAppIdsOrdered() {
|
||||
// Get unique app IDs in order they appear in the Niri layout
|
||||
// Windows are sorted by workspace and then by position within the workspace
|
||||
var sortedWindows = [...windows].sort((a, b) => {
|
||||
// If both have layout info, sort by position
|
||||
if (a.layout && b.layout
|
||||
&& a.layout.pos_in_scrolling_layout
|
||||
&& b.layout.pos_in_scrolling_layout) {
|
||||
var aPos = a.layout.pos_in_scrolling_layout
|
||||
var bPos = b.layout.pos_in_scrolling_layout
|
||||
|
||||
// First compare workspace index
|
||||
if (aPos[0] !== bPos[0]) {
|
||||
return aPos[0] - bPos[0]
|
||||
}
|
||||
// Then compare position within workspace
|
||||
return aPos[1] - bPos[1]
|
||||
}
|
||||
// Fallback to window ID if no layout info
|
||||
return a.id - b.id
|
||||
})
|
||||
|
||||
var appIds = []
|
||||
var seenApps = new Set()
|
||||
|
||||
sortedWindows.forEach(w => {
|
||||
if (w.app_id) {
|
||||
var lowerAppId = w.app_id.toLowerCase()
|
||||
if (!seenApps.has(lowerAppId)) {
|
||||
appIds.push(lowerAppId)
|
||||
seenApps.add(lowerAppId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return appIds
|
||||
}
|
||||
}
|
||||
|
||||
13
shell.qml
13
shell.qml
@@ -44,11 +44,9 @@ ShellRoot {
|
||||
delegate: Dock {
|
||||
modelData: item
|
||||
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
|
||||
windowsMenu: dockWindowsMenuLoader.item ? dockWindowsMenuLoader.item : null
|
||||
|
||||
Component.onCompleted: {
|
||||
dockContextMenuLoader.active = true
|
||||
dockWindowsMenuLoader.active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,14 +70,6 @@ ShellRoot {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: dockWindowsMenuLoader
|
||||
active: false
|
||||
|
||||
DockWindowsMenu {
|
||||
id: dockWindowsMenu
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: notificationCenterLoader
|
||||
@@ -258,6 +248,9 @@ ShellRoot {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
|
||||
Reference in New Issue
Block a user