diff --git a/Config/Config.qml b/Config/Config.qml index ef7b115..4d0bcad 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -160,7 +160,8 @@ Singleton { hoverRegionHeight: dock.hoverRegionHeight, hoverToReveal: dock.hoverToReveal, pinnedApps: dock.pinnedApps, - pinnedOnStartup: dock.pinnedOnStartup + pinnedOnStartup: dock.pinnedOnStartup, + ignoredAppRegexes: dock.ignoredAppRegexes }; } diff --git a/Config/DockConfig.qml b/Config/DockConfig.qml index 135a3e0..36cc75d 100644 --- a/Config/DockConfig.qml +++ b/Config/DockConfig.qml @@ -5,6 +5,7 @@ JsonObject { property real height: 60 property real hoverRegionHeight: 2 property bool hoverToReveal: true + property list ignoredAppRegexes: [] property list pinnedApps: ["org.kde.dolphin", "kitty",] property bool pinnedOnStartup: false } diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index c7abccc..8c1fcef 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -13,6 +13,7 @@ import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources import qs.Modules.Drawing as Drawing import qs.Modules.Settings as Settings +import qs.Modules.Dock as Dock Shape { id: root @@ -24,6 +25,7 @@ Shape { anchors.fill: parent anchors.margins: Config.barConfig.border anchors.topMargin: bar.implicitHeight + asynchronous: true preferredRendererType: Shape.CurveRenderer Drawing.Background { @@ -93,4 +95,12 @@ Shape { startY: 0 wrapper: root.panels.settings } + + Dock.Background { + id: dock + + startX: (root.width - wrapper.width) / 2 - rounding + startY: root.height + wrapper: root.panels.dock + } } diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 25f3077..5116422 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -107,6 +107,9 @@ CustomMouseArea { } } + if (!visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y)) + visibilities.dock = true; + if (y < root.bar.implicitHeight) { root.bar.checkPopout(x); } @@ -145,6 +148,9 @@ CustomMouseArea { root.panels.osd.hovered = false; } } + + if (root.visibilities.launcher) + root.visibilities.dock = false; } function onOsdChanged() { diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 32b64f9..4aa5000 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -12,6 +12,7 @@ import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources import qs.Modules.Settings as Settings import qs.Modules.Drawing as Drawing +import qs.Modules.Dock as Dock import qs.Config Item { @@ -19,6 +20,7 @@ Item { required property Item bar readonly property alias dashboard: dashboard + readonly property alias dock: dock readonly property alias drawing: drawing required property Canvas drawingItem readonly property alias launcher: launcher @@ -143,4 +145,14 @@ Item { panels: root visibilities: root.visibilities } + + Dock.Wrapper { + id: dock + + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + panels: root + screen: root.screen + visibilities: root.visibilities + } } diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index a47e4a5..e38a84a 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -33,7 +33,7 @@ Variants { property var root: Quickshell.shellDir WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None color: "transparent" contentItem.focus: true mask: visibilities.isDrawing ? null : region @@ -94,7 +94,7 @@ Variants { HyprlandFocusGrab { id: focusGrab - active: visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) + active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) windows: [win] onCleared: { @@ -104,6 +104,7 @@ Variants { visibilities.osd = false; visibilities.settings = false; visibilities.resources = false; + visibilities.dock = false; panels.popouts.hasCurrent = false; } } @@ -113,6 +114,7 @@ Variants { property bool bar property bool dashboard + property bool dock property bool isDrawing property bool launcher property bool notif: NotifServer.popups.length > 0 diff --git a/Helpers/TaskbarApps.qml b/Helpers/TaskbarApps.qml new file mode 100644 index 0000000..4e19d3f --- /dev/null +++ b/Helpers/TaskbarApps.qml @@ -0,0 +1,86 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Config + +Singleton { + id: root + + property list apps: { + var map = new Map(); + + // Pinned apps + const pinnedApps = Config.dock.pinnedApps ?? []; + for (const appId of pinnedApps) { + if (!map.has(appId.toLowerCase())) + map.set(appId.toLowerCase(), ({ + pinned: true, + toplevels: [] + })); + } + + // Separator + if (pinnedApps.length > 0) { + map.set("SEPARATOR", { + pinned: false, + toplevels: [] + }); + } + + // Ignored apps + const ignoredRegexStrings = Config.dock.ignoredAppRegexes ?? []; + const ignoredRegexes = ignoredRegexStrings.map(pattern => new RegExp(pattern, "i")); + // Open windows + for (const toplevel of ToplevelManager.toplevels.values) { + if (ignoredRegexes.some(re => re.test(toplevel.appId))) + continue; + if (!map.has(toplevel.appId.toLowerCase())) + map.set(toplevel.appId.toLowerCase(), ({ + pinned: false, + toplevels: [] + })); + map.get(toplevel.appId.toLowerCase()).toplevels.push(toplevel); + } + + var values = []; + + for (const [key, value] of map) { + values.push(appEntryComp.createObject(null, { + appId: key, + toplevels: value.toplevels, + pinned: value.pinned + })); + } + + return values; + } + + function isPinned(appId) { + return Config.dock.pinnedApps.indexOf(appId) !== -1; + } + + function togglePin(appId) { + if (root.isPinned(appId)) { + Config.dock.pinnedApps = Config.dock.pinnedApps.filter(id => id !== appId); + } else { + Config.dock.pinnedApps = Config.dock.pinnedApps.concat([appId]); + } + } + + Component { + id: appEntryComp + + TaskbarAppEntry { + } + } + + component TaskbarAppEntry: QtObject { + id: wrapper + + required property string appId + required property bool pinned + required property list toplevels + } +} diff --git a/Modules/Dock/Content.qml b/Modules/Dock/Content.qml index 6089700..2b28f55 100644 --- a/Modules/Dock/Content.qml +++ b/Modules/Dock/Content.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick +import qs.Modules.Dock.Parts import qs.Components import qs.Helpers import qs.Config @@ -17,12 +18,29 @@ Item { implicitHeight: Config.dock.height + root.padding * 2 implicitWidth: dockRow.implicitWidth + root.padding * 2 - RowLayout { + CustomListView { id: dockRow - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: Appearance.spacing.small + anchors.centerIn: parent + implicitHeight: Config.dock.height + implicitWidth: contentWidth + orientation: ListView.Horizontal + spacing: Appearance.padding.smaller + + delegate: DockAppButton { + required property var modelData + + appListRoot: root + appToplevel: modelData + visibilities: root.visibilities + } + Behavior on implicitWidth { + Anim { + } + } + model: ScriptModel { + objectProp: "appId" + values: TaskbarApps.apps + } } } diff --git a/Modules/Dock/Parts/DockAppButton.qml b/Modules/Dock/Parts/DockAppButton.qml new file mode 100644 index 0000000..98b1972 --- /dev/null +++ b/Modules/Dock/Parts/DockAppButton.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Components +import qs.Helpers +import qs.Config + +CustomRect { + id: root + + property bool appIsActive: appToplevel.toplevels.find(t => (t.activated == true)) !== undefined + property var appListRoot + property var appToplevel + property real countDotHeight: 4 + property real countDotWidth: 10 + property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId) + property real iconSize: implicitHeight - 20 + readonly property bool isSeparator: appToplevel.appId === "SEPARATOR" + property int lastFocused: -1 + required property PersistentProperties visibilities + + implicitHeight: Config.dock.height + implicitWidth: isSeparator ? 1 : implicitHeight + radius: Appearance.rounding.normal - Appearance.padding.small + + Loader { + active: !isSeparator + anchors.centerIn: parent + + sourceComponent: ColumnLayout { + IconImage { + id: icon + + Layout.alignment: Qt.AlignHCenter + implicitSize: root.iconSize + source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing") + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 3 + + Repeater { + model: Math.min(appToplevel.toplevels.length, 3) + + delegate: Rectangle { + required property int index + + color: appIsActive ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3primary + implicitHeight: root.countDotHeight + implicitWidth: (appToplevel.toplevels.length <= 3) ? root.countDotWidth : root.countDotHeight // Circles when too many + radius: Appearance.rounding.full + } + } + } + } + } + + StateLayer { + onClicked: { + if (appToplevel.toplevels.length === 0) { + root.desktopEntry?.execute(); + root.visibilities.dock = false; + return; + } + lastFocused = (lastFocused + 1) % appToplevel.toplevels.length; + appToplevel.toplevels[lastFocused].activate(); + root.visibilities.dock = false; + } + } + + Connections { + function onApplicationsChanged() { + root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel.appId); + } + + target: DesktopEntries + } + + Loader { + active: isSeparator + + sourceComponent: DockSeparator { + } + + anchors { + fill: parent + } + } +} diff --git a/Modules/Dock/Parts/DockSeparator.qml b/Modules/Dock/Parts/DockSeparator.qml new file mode 100644 index 0000000..b535412 --- /dev/null +++ b/Modules/Dock/Parts/DockSeparator.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + Layout.bottomMargin: dockRow.padding + Appearance.rounding.normal + Layout.fillHeight: true + Layout.topMargin: dockRow.padding + Appearance.rounding.normal + color: DynamicColors.palette.m3outlineVariant + implicitWidth: 1 +}