diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml new file mode 100644 index 00000000..620e92e4 --- /dev/null +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -0,0 +1,256 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property bool density: Settings.data.bar.density + readonly property real itemSize: (density === "compact") ? Style.capsuleHeight * 0.9 : Style.capsuleHeight * 0.8 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : false + property ListModel localWorkspaces: ListModel {} + + function getWindowsForWorkspace(workspaceId) { + var windowsInWs = [] + for (var i = 0; i < CompositorService.windows.count; i++) { + var window = CompositorService.windows.get(i) + if (window.workspaceId === workspaceId) { + windowsInWs.push(window) + } + } + return windowsInWs + } + + function refreshWorkspaces() { + localWorkspaces.clear() + if (screen !== null) { + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i) + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) { + continue + } + + var windowsInWs = getWindowsForWorkspace(ws.id) + var windowsModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', root) + for (var j = 0; j < windowsInWs.length; j++) { + windowsModel.append(windowsInWs[j]) + } + localWorkspaces.append({ + "id": ws.id, + "name": ws.name, + "output": ws.output, + "isFocused": ws.isFocused, + "isOccupied": ws.isOccupied, + "isActive": ws.isActive, + "isUrgent": ws.isUrgent, + "windows": windowsModel + }) + } + } + } + } + + Component.onCompleted: { + refreshWorkspaces() + } + implicitWidth: isVerticalBar ? taskbarLayoutVertical.implicitWidth + Style.marginM * 2 : Math.round(taskbarLayoutHorizontal.implicitWidth + Style.marginM * 2) + implicitHeight: isVerticalBar ? Math.round(taskbarLayoutVertical.implicitHeight + Style.marginM * 2) : Style.barHeight + + Connections { + target: CompositorService + + function onWorkspacesChanged() { + refreshWorkspaces() + } + + function onWindowListChanged() { + refreshWorkspaces() + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + y: isVerticalBar ? 0 : (parent.height - height) / 2 + height: isVerticalBar ? parent.height : Style.capsuleHeight + radius: Style.radiusM + color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent + } + + Component { + id: workspaceRepeaterDelegate + + Rectangle { + id: container + + property var workspaceModel: model + property bool hasWindows: workspaceModel.windows.count > 0 + + radius: Style.radiusM + color: "transparent" + border.color: workspaceModel.isFocused ? Color.mPrimary : Color.mOutline + border.width: 1 + // Dynamic sizing + width: (hasWindows ? iconsFlow.implicitWidth : root.itemSize * 0.8) + Style.marginL + height: (hasWindows ? iconsFlow.implicitHeight : root.itemSize * 0.8) + Style.marginXS + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: !hasWindows + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + CompositorService.switchToWorkspace(workspaceModel) + } + } + + Flow { + id: iconsFlow + + anchors.centerIn: parent + spacing: 4 + flow: root.isVerticalBar ? Flow.TopToBottom : Flow.LeftToRight + + Repeater { + model: workspaceModel.windows + + delegate: Item { + id: taskbarItem + + width: root.itemSize * 0.8 + height: root.itemSize * 0.8 + + IconImage { + id: appIcon + + width: parent.width + height: parent.height + source: ThemeIcons.iconForAppId(model.appId) + smooth: true + asynchronous: true + opacity: model.isFocused ? Style.opacityFull : 0.6 + layer.enabled: widgetSettings.colorizeIcons === true + + Rectangle { + anchors.bottomMargin: -2 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: 4 + height: 4 + color: model.isFocused ? Color.mPrimary : Color.transparent + radius: width * 0.5 + } + + layer.effect: ShaderEffect { + property color targetColor: Color.mOnSurface + property real colorizeMode: 0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: function (mouse) { + if (!model) { + return + } + + if (mouse.button === Qt.LeftButton) { + try { + CompositorService.focusWindow(model) + } catch (error) { + Logger.error("TaskbarGrouped", "Failed to focus window: " + error) + } + } else if (mouse.button === Qt.RightButton) { + try { + CompositorService.closeWindow(model) + } catch (error) { + Logger.error("TaskbarGrouped", "Failed to close window: " + error) + } + } + } + onEntered: TooltipService.show(Screen, taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection()) + onExited: TooltipService.hide() + } + } + } + } + + // Animate size changes for a smooth look + Behavior on width { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + } + } + + Row { + id: taskbarLayoutHorizontal + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Style.marginM + spacing: Style.marginS + visible: !isVerticalBar + + Repeater { + model: localWorkspaces + delegate: workspaceRepeaterDelegate + } + } + + Column { + id: taskbarLayoutVertical + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Style.marginM + spacing: Style.marginS + visible: isVerticalBar + + Repeater { + model: localWorkspaces + delegate: workspaceRepeaterDelegate + } + } +} \ No newline at end of file diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index a027df5f..f825f2b5 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -30,6 +30,7 @@ Singleton { "Spacer": spacerComponent, "SystemMonitor": systemMonitorComponent, "Taskbar": taskbarComponent, + "TaskbarGrouped": taskbarGroupedComponent, "Tray": trayComponent, "Volume": volumeComponent, "WiFi": wiFiComponent, @@ -128,6 +129,9 @@ Singleton { "hideMode": "hidden", "colorizeIcons": false }, + "TaskbarGrouped": { + "allowUserSettings": true + }, "Tray": { "allowUserSettings": true, "blacklist": [], @@ -224,6 +228,9 @@ Singleton { property Component taskbarComponent: Component { Taskbar {} } + property Component taskbarGroupedComponent: Component { + TaskbarGrouped {} + } function init() { Logger.i("BarWidgetRegistry", "Service started")