summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets
diff options
context:
space:
mode:
authornippy <you@example.com>2026-04-18 13:49:56 +0200
committernippy <you@example.com>2026-04-18 13:49:56 +0200
commit5d94c0a7d44a2255b81815a52a7056a94a39842d (patch)
tree759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets
parente79cdf210b267f21a186255ce1a4d50029439d54 (diff)
downloadRaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz
RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip
update Raveos themes
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDock.qml1011
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml486
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockOverflowButton.qml76
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml94
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml165
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CapsLockIndicator.qml62
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ClipboardButton.qml314
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Clock.qml331
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ColorPicker.qml37
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml830
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuMonitor.qml146
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuTemperature.qml140
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DWLLayout.qml110
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DiskUsage.qml255
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/FocusedApp.qml317
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/GpuTemperature.qml224
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/IdleInhibitor.qml36
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml268
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/LauncherButton.qml123
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Media.qml502
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NetworkMonitor.qml147
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotepadButton.qml367
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml40
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml25
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PrivacyIndicator.qml247
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RamMonitor.qml175
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RunningApps.qml902
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml1893
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml187
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Vpn.qml141
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Weather.qml80
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml1981
32 files changed, 11712 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDock.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDock.qml
new file mode 100644
index 0000000..294102d
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDock.qml
@@ -0,0 +1,1011 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Widgets
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ enableBackgroundHover: false
+ enableCursor: false
+ section: "left"
+
+ property var widgetData: null
+ property var hoveredItem: null
+ property var topBar: null
+ property bool isAutoHideBar: false
+ property Item windowRoot: (Window.window ? Window.window.contentItem : null)
+
+ property int draggedIndex: -1
+ property int dropTargetIndex: -1
+ property bool suppressShiftAnimation: false
+ property int pinnedAppCount: 0
+
+ property int maxVisibleApps: widgetData?.barMaxVisibleApps !== undefined ? widgetData.barMaxVisibleApps : SettingsData.barMaxVisibleApps
+ property int maxVisibleRunningApps: widgetData?.barMaxVisibleRunningApps !== undefined ? widgetData.barMaxVisibleRunningApps : SettingsData.barMaxVisibleRunningApps
+ property bool showOverflowBadge: widgetData?.barShowOverflowBadge !== undefined ? widgetData.barShowOverflowBadge : SettingsData.barShowOverflowBadge
+ property bool overflowExpanded: false
+ property int overflowItemCount: 0
+
+ onMaxVisibleAppsChanged: updateModel()
+ onMaxVisibleRunningAppsChanged: updateModel()
+
+ readonly property real effectiveBarThickness: {
+ if (barThickness > 0 && barSpacing > 0) {
+ return barThickness + barSpacing;
+ }
+ const innerPadding = barConfig?.innerPadding ?? 4;
+ const spacing = barConfig?.spacing ?? 4;
+ return Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding)) + spacing;
+ }
+
+ readonly property var barBounds: {
+ if (!parentScreen || !barConfig) {
+ return {
+ "x": 0,
+ "y": 0,
+ "width": 0,
+ "height": 0,
+ "wingSize": 0
+ };
+ }
+ const barPosition = axis.edge === "left" ? 2 : (axis.edge === "right" ? 3 : (axis.edge === "top" ? 0 : 1));
+ return SettingsData.getBarBounds(parentScreen, effectiveBarThickness, barPosition, barConfig);
+ }
+
+ readonly property real barY: barBounds.y
+
+ readonly property real minTooltipY: {
+ if (!parentScreen || !isVerticalOrientation) {
+ return 0;
+ }
+
+ if (isAutoHideBar) {
+ return 0;
+ }
+
+ if (parentScreen.y > 0) {
+ return effectiveBarThickness;
+ }
+
+ return 0;
+ }
+
+ // --- Dock Logic Helpers ---
+ function movePinnedApp(fromDockIndex, toDockIndex) {
+ if (fromDockIndex === toDockIndex)
+ return;
+
+ const currentPinned = [...(SessionData.barPinnedApps || [])];
+ if (fromDockIndex < 0 || fromDockIndex >= currentPinned.length || toDockIndex < 0 || toDockIndex >= currentPinned.length) {
+ return;
+ }
+
+ const movedApp = currentPinned.splice(fromDockIndex, 1)[0];
+ currentPinned.splice(toDockIndex, 0, movedApp);
+
+ SessionData.setBarPinnedApps(currentPinned);
+ }
+
+ property int _desktopEntriesUpdateTrigger: 0
+ property int _toplevelsUpdateTrigger: 0
+ property int _appIdSubstitutionsTrigger: 0
+
+ Connections {
+ target: CompositorService
+ function onToplevelsChanged() {
+ _toplevelsUpdateTrigger++;
+ updateModel();
+ }
+ }
+
+ Connections {
+ target: DesktopEntries
+ function onApplicationsChanged() {
+ _desktopEntriesUpdateTrigger++;
+ }
+ }
+
+ Connections {
+ target: SettingsData
+ function onAppIdSubstitutionsChanged() {
+ _appIdSubstitutionsTrigger++;
+ updateModel();
+ }
+ function onRunningAppsCurrentWorkspaceChanged() {
+ updateModel();
+ }
+ function onBarMaxVisibleAppsChanged() {
+ updateModel();
+ }
+ function onBarMaxVisibleRunningAppsChanged() {
+ updateModel();
+ }
+ }
+
+ Connections {
+ target: SessionData
+ function onBarPinnedAppsChanged() {
+ root.suppressShiftAnimation = true;
+ root.draggedIndex = -1;
+ root.dropTargetIndex = -1;
+ updateModel();
+ Qt.callLater(() => {
+ root.suppressShiftAnimation = false;
+ });
+ }
+ }
+
+ property var dockItems: []
+
+ function isOnScreen(toplevel, screenName) {
+ if (!toplevel.screens)
+ return false;
+ for (let i = 0; i < toplevel.screens.length; i++) {
+ if (toplevel.screens[i]?.name === screenName)
+ return true;
+ }
+ return false;
+ }
+
+ function getCoreAppData(appId) {
+ if (typeof AppSearchService === "undefined")
+ return null;
+ const coreApps = AppSearchService.coreApps || [];
+ for (let i = 0; i < coreApps.length; i++) {
+ if (coreApps[i].builtInPluginId === appId)
+ return coreApps[i];
+ }
+ return null;
+ }
+
+ function getCoreAppDataByTitle(windowTitle) {
+ if (typeof AppSearchService === "undefined" || !windowTitle)
+ return null;
+ const coreApps = AppSearchService.coreApps || [];
+ for (let i = 0; i < coreApps.length; i++) {
+ if (coreApps[i].name === windowTitle)
+ return coreApps[i];
+ }
+ return null;
+ }
+
+ function createSeparator(key) {
+ return {
+ uniqueKey: key,
+ type: "separator",
+ appId: "__SEPARATOR__",
+ toplevel: null,
+ isPinned: false,
+ isRunning: false,
+ isInOverflow: false
+ };
+ }
+
+ function markAsOverflow(item) {
+ return {
+ uniqueKey: item.uniqueKey,
+ type: item.type,
+ appId: item.appId,
+ toplevel: item.toplevel,
+ isPinned: item.isPinned,
+ isRunning: item.isRunning,
+ windowCount: item.windowCount,
+ allWindows: item.allWindows,
+ isCoreApp: item.isCoreApp,
+ coreAppData: item.coreAppData,
+ isInOverflow: true
+ };
+ }
+
+ function markAsVisible(item) {
+ return {
+ uniqueKey: item.uniqueKey,
+ type: item.type,
+ appId: item.appId,
+ toplevel: item.toplevel,
+ isPinned: item.isPinned,
+ isRunning: item.isRunning,
+ windowCount: item.windowCount,
+ allWindows: item.allWindows,
+ isCoreApp: item.isCoreApp,
+ coreAppData: item.coreAppData,
+ isInOverflow: false
+ };
+ }
+
+ function buildBaseItems() {
+ const items = [];
+ const pinnedApps = [...(SessionData.barPinnedApps || [])];
+ _toplevelsUpdateTrigger;
+ const allToplevels = CompositorService.sortedToplevels;
+
+ let sortedToplevels = allToplevels;
+ if (SettingsData.runningAppsCurrentWorkspace && parentScreen) {
+ sortedToplevels = CompositorService.filterCurrentWorkspace(allToplevels, parentScreen.name) || [];
+ }
+
+ const appGroups = new Map();
+
+ pinnedApps.forEach(rawAppId => {
+ const appId = Paths.moddedAppId(rawAppId);
+ const coreAppData = getCoreAppData(appId);
+ appGroups.set(appId, {
+ appId: appId,
+ isPinned: true,
+ windows: [],
+ isCoreApp: coreAppData !== null,
+ coreAppData: coreAppData
+ });
+ });
+
+ sortedToplevels.forEach((toplevel, index) => {
+ const rawAppId = toplevel.appId || "unknown";
+ let appId = Paths.moddedAppId(rawAppId);
+
+ let coreAppData = null;
+ if (rawAppId === "org.quickshell" || rawAppId === "com.danklinux.dms") {
+ coreAppData = getCoreAppDataByTitle(toplevel.title);
+ if (coreAppData) {
+ appId = coreAppData.builtInPluginId;
+ }
+ }
+
+ if (!appGroups.has(appId)) {
+ appGroups.set(appId, {
+ appId: appId,
+ isPinned: false,
+ windows: [],
+ isCoreApp: coreAppData !== null,
+ coreAppData: coreAppData
+ });
+ }
+
+ appGroups.get(appId).windows.push({
+ toplevel: toplevel,
+ index: index,
+ windowTitle: toplevel.title
+ });
+ });
+
+ const pinnedGroups = [];
+ const unpinnedGroups = [];
+
+ Array.from(appGroups.entries()).forEach(([appId, group]) => {
+ const firstWindow = group.windows.length > 0 ? group.windows[0] : null;
+
+ const item = {
+ uniqueKey: "grouped_" + appId,
+ type: "grouped",
+ appId: appId,
+ toplevel: firstWindow ? firstWindow.toplevel : null,
+ isPinned: group.isPinned,
+ isRunning: group.windows.length > 0,
+ windowCount: group.windows.length,
+ allWindows: group.windows,
+ isCoreApp: group.isCoreApp || false,
+ coreAppData: group.coreAppData || null,
+ isInOverflow: false
+ };
+
+ if (group.isPinned) {
+ pinnedGroups.push(item);
+ } else {
+ unpinnedGroups.push(item);
+ }
+ });
+
+ pinnedGroups.forEach(item => items.push(item));
+
+ if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
+ items.push(createSeparator("separator_grouped"));
+ }
+
+ unpinnedGroups.forEach(item => items.push(item));
+
+ root.pinnedAppCount = pinnedGroups.length;
+ return {
+ items,
+ pinnedCount: pinnedGroups.length,
+ runningCount: unpinnedGroups.length
+ };
+ }
+
+ function applyOverflow(baseResult) {
+ const {
+ items
+ } = baseResult;
+ const maxPinned = root.maxVisibleApps;
+ const maxRunning = root.maxVisibleRunningApps;
+
+ const pinnedItems = items.filter(i => i.type === "grouped" && i.isPinned);
+ const runningItems = items.filter(i => i.type === "grouped" && i.isRunning && !i.isPinned);
+
+ const pinnedOverflow = maxPinned > 0 && pinnedItems.length > maxPinned;
+ const runningOverflow = maxRunning > 0 && runningItems.length > maxRunning;
+
+ if (!pinnedOverflow && !runningOverflow) {
+ root.overflowItemCount = 0;
+ return items.map(i => markAsVisible(i));
+ }
+
+ const visiblePinnedKeys = new Set(pinnedOverflow ? pinnedItems.slice(0, maxPinned).map(i => i.uniqueKey) : pinnedItems.map(i => i.uniqueKey));
+ const visibleRunningKeys = new Set(runningOverflow ? runningItems.slice(0, maxRunning).map(i => i.uniqueKey) : runningItems.map(i => i.uniqueKey));
+
+ const overflowPinnedCount = pinnedOverflow ? pinnedItems.length - maxPinned : 0;
+ const overflowRunningCount = runningOverflow ? runningItems.length - maxRunning : 0;
+ const totalOverflow = overflowPinnedCount + overflowRunningCount;
+ root.overflowItemCount = totalOverflow;
+
+ const finalItems = [];
+ let addedSeparator = false;
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ switch (item.type) {
+ case "separator":
+ break;
+ case "grouped":
+ if (item.isPinned) {
+ if (visiblePinnedKeys.has(item.uniqueKey)) {
+ finalItems.push(markAsVisible(item));
+ } else {
+ finalItems.push(markAsOverflow(item));
+ }
+ } else if (item.isRunning) {
+ if (!addedSeparator && finalItems.length > 0) {
+ finalItems.push(createSeparator("separator_overflow"));
+ addedSeparator = true;
+ }
+ if (visibleRunningKeys.has(item.uniqueKey)) {
+ finalItems.push(markAsVisible(item));
+ } else {
+ finalItems.push(markAsOverflow(item));
+ }
+ }
+ break;
+ default:
+ finalItems.push(item);
+ break;
+ }
+ }
+
+ if (totalOverflow > 0) {
+ const toggleIndex = finalItems.findIndex(i => i.type === "separator");
+ const insertPos = toggleIndex >= 0 ? toggleIndex : finalItems.length;
+ finalItems.splice(insertPos, 0, {
+ uniqueKey: "overflow_toggle",
+ type: "overflow-toggle",
+ appId: "__OVERFLOW_TOGGLE__",
+ toplevel: null,
+ isPinned: false,
+ isRunning: false,
+ isInOverflow: false,
+ overflowCount: totalOverflow
+ });
+ }
+
+ return finalItems;
+ }
+
+ function updateModel() {
+ const baseResult = buildBaseItems();
+ dockItems = applyOverflow(baseResult);
+ }
+
+ Component.onCompleted: updateModel()
+
+ visible: dockItems.length > 0
+ readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6
+
+ content: Component {
+ Item {
+ implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
+ implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
+
+ Loader {
+ id: layoutLoader
+ anchors.centerIn: parent
+ sourceComponent: root.isVerticalOrientation ? columnLayout : rowLayout
+ }
+ }
+ }
+
+ Component {
+ id: rowLayout
+ Row {
+ spacing: Theme.spacingXS
+
+ Repeater {
+ id: repeater
+ model: ScriptModel {
+ values: root.dockItems
+ objectProp: "uniqueKey"
+ }
+
+ delegate: dockDelegate
+ }
+ }
+ }
+
+ Component {
+ id: columnLayout
+ Column {
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: ScriptModel {
+ values: root.dockItems
+ objectProp: "uniqueKey"
+ }
+ delegate: dockDelegate
+ }
+ }
+ }
+
+ Loader {
+ id: tooltipLoader
+ active: false
+ sourceComponent: DankTooltip {}
+ }
+
+ Component {
+ id: dockDelegate
+ Item {
+ id: delegateItem
+ property bool isSeparator: modelData.type === "separator"
+ readonly property bool isOverflowToggle: modelData.type === "overflow-toggle"
+ readonly property bool isInOverflow: modelData.isInOverflow === true
+
+ readonly property real visualSize: isSeparator ? 8 : ((widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120))
+ readonly property real visualWidth: root.isVerticalOrientation ? root.barThickness : visualSize
+ readonly property real visualHeight: root.isVerticalOrientation ? visualSize : root.barThickness
+
+ visible: !isInOverflow || root.overflowExpanded
+ opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1
+ scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1
+
+ width: (isInOverflow && !root.overflowExpanded) ? 0 : visualWidth
+ height: (isInOverflow && !root.overflowExpanded) ? 0 : visualHeight
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ z: (dragHandler.dragging) ? 100 : 0
+
+ // --- Drag and Drop Shift Animation Logic ---
+ property real shiftOffset: {
+ if (root.draggedIndex < 0 || !modelData.isPinned || isSeparator)
+ return 0;
+ if (index === root.draggedIndex)
+ return 0;
+
+ const dragIdx = root.draggedIndex;
+ const dropIdx = root.dropTargetIndex;
+ const myIdx = index;
+ const shiftAmount = visualSize + Theme.spacingXS;
+
+ if (dropIdx < 0)
+ return 0;
+ if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx)
+ return -shiftAmount;
+ if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx)
+ return shiftAmount;
+ return 0;
+ }
+
+ transform: Translate {
+ x: root.isVerticalOrientation ? 0 : delegateItem.shiftOffset
+ y: root.isVerticalOrientation ? delegateItem.shiftOffset : 0
+
+ Behavior on x {
+ enabled: !root.suppressShiftAnimation
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutCubic
+ }
+ }
+ Behavior on y {
+ enabled: !root.suppressShiftAnimation
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ Rectangle {
+ visible: isSeparator
+ width: root.isVerticalOrientation ? root.barThickness * 0.6 : 2
+ height: root.isVerticalOrientation ? 2 : root.barThickness * 0.6
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
+ radius: 1
+ anchors.centerIn: parent
+ }
+
+ AppsDockOverflowButton {
+ visible: isOverflowToggle
+ anchors.centerIn: parent
+ width: delegateItem.visualWidth
+ height: delegateItem.visualHeight
+ iconSize: 24
+ overflowCount: modelData.overflowCount || 0
+ overflowExpanded: root.overflowExpanded
+ isVertical: root.isVerticalOrientation
+ showBadge: root.showOverflowBadge
+ z: 10
+ onClicked: {
+ console.log("Overflow button clicked! Current state:", root.overflowExpanded);
+ root.overflowExpanded = !root.overflowExpanded;
+ console.log("New state:", root.overflowExpanded);
+ }
+ }
+
+ Item {
+ id: appItem
+ visible: !isSeparator && !isOverflowToggle
+ anchors.fill: parent
+
+ property bool isFocused: {
+ if (modelData.type === "grouped") {
+ return modelData.allWindows.some(w => w.toplevel && w.toplevel.activated);
+ }
+ return modelData.toplevel ? modelData.toplevel.activated : false;
+ }
+
+ property var appId: modelData.appId
+ property int windowCount: modelData.windowCount || (modelData.isRunning ? 1 : 0)
+ property string windowTitle: {
+ if (modelData.type === "grouped") {
+ const active = modelData.allWindows.find(w => w.toplevel && w.toplevel.activated);
+ if (active)
+ return active.windowTitle || "(Unnamed)";
+ if (modelData.allWindows.length > 0)
+ return modelData.allWindows[0].windowTitle || "(Unnamed)";
+ return "";
+ }
+ return modelData.toplevel ? (modelData.toplevel.title || "(Unnamed)") : "";
+ }
+
+ property string tooltipText: {
+ root._desktopEntriesUpdateTrigger;
+ const desktopEntry = appId ? DesktopEntries.heuristicLookup(appId) : null;
+ const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown";
+
+ if (modelData.type === "grouped" && windowCount > 1) {
+ return appName + " (" + windowCount + " windows)";
+ }
+ return appName + (windowTitle ? " • " + windowTitle : "");
+ }
+
+ readonly property bool enlargeEnabled: (widgetData?.appsDockEnlargeOnHover !== undefined ? widgetData.appsDockEnlargeOnHover : SettingsData.appsDockEnlargeOnHover)
+ readonly property real enlargeScale: enlargeEnabled && mouseArea.containsMouse ? (widgetData?.appsDockEnlargePercentage !== undefined ? widgetData.appsDockEnlargePercentage : SettingsData.appsDockEnlargePercentage) / 100.0 : 1.0
+ readonly property real baseIconSizeMultiplier: (widgetData?.appsDockIconSizePercentage !== undefined ? widgetData.appsDockIconSizePercentage : SettingsData.appsDockIconSizePercentage) / 100.0
+ readonly property real effectiveIconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) * baseIconSizeMultiplier
+
+ readonly property color activeOverlayColor: {
+ switch (SettingsData.appsDockActiveColorMode) {
+ case "secondary":
+ return Theme.secondary;
+ case "primaryContainer":
+ return Theme.primaryContainer;
+ case "error":
+ return Theme.error;
+ case "success":
+ return Theme.success;
+ default:
+ return Theme.primary;
+ }
+ }
+
+ transform: Translate {
+ x: (dragHandler.dragging && !root.isVerticalOrientation) ? dragHandler.dragAxisOffset : 0
+ y: (dragHandler.dragging && root.isVerticalOrientation) ? dragHandler.dragAxisOffset : 0
+ }
+
+ Rectangle {
+ id: visualContent
+ width: root.isVerticalOrientation ? root.iconCellSize : delegateItem.visualSize
+ height: root.isVerticalOrientation ? delegateItem.visualSize : root.iconCellSize
+ anchors.centerIn: parent
+ radius: Theme.cornerRadius
+ color: {
+ const colorizeEnabled = (widgetData?.appsDockColorizeActive !== undefined ? widgetData.appsDockColorizeActive : SettingsData.appsDockColorizeActive);
+
+ if (appItem.isFocused && colorizeEnabled) {
+ return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3);
+ }
+ return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
+ }
+
+ border.width: dragHandler.dragging ? 2 : 0
+ border.color: Theme.primary
+ opacity: dragHandler.dragging ? 0.8 : 1.0
+
+ AppIconRenderer {
+ id: coreIcon
+ readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
+ anchors.left: (root.isVerticalOrientation || isCompact) ? undefined : parent.left
+ anchors.leftMargin: (root.isVerticalOrientation || isCompact) ? 0 : Theme.spacingXS
+ anchors.top: (root.isVerticalOrientation && !isCompact) ? parent.top : undefined
+ anchors.topMargin: (root.isVerticalOrientation && !isCompact) ? Theme.spacingXS : 0
+ anchors.centerIn: (root.isVerticalOrientation || isCompact) ? parent : undefined
+
+ iconSize: appItem.effectiveIconSize
+ materialIconSizeAdjustment: 0
+ iconValue: {
+ if (!modelData || !modelData.isCoreApp || !modelData.coreAppData)
+ return "";
+ const appId = modelData.coreAppData.id || modelData.coreAppData.builtInPluginId;
+ if ((appId === "dms_settings" || appId === "dms_notepad" || appId === "dms_sysmon") && modelData.coreAppData.cornerIcon) {
+ return "material:" + modelData.coreAppData.cornerIcon;
+ }
+ return modelData.coreAppData.icon || "";
+ }
+ colorOverride: Theme.widgetIconColor
+ fallbackText: "?"
+ visible: iconValue !== ""
+ z: 2
+
+ transformOrigin: Item.Center
+ scale: appItem.enlargeScale
+ Behavior on scale {
+ NumberAnimation {
+ duration: 120
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ IconImage {
+ id: iconImg
+ readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
+ anchors.left: (root.isVerticalOrientation || isCompact) ? undefined : parent.left
+ anchors.leftMargin: (root.isVerticalOrientation || isCompact) ? 0 : Theme.spacingXS
+ anchors.top: (root.isVerticalOrientation && !isCompact) ? parent.top : undefined
+ anchors.topMargin: (root.isVerticalOrientation && !isCompact) ? Theme.spacingXS : 0
+ anchors.centerIn: (root.isVerticalOrientation || isCompact) ? parent : undefined
+
+ width: appItem.effectiveIconSize
+ height: appItem.effectiveIconSize
+ source: {
+ root._desktopEntriesUpdateTrigger;
+ root._appIdSubstitutionsTrigger;
+ if (!appItem.appId)
+ return "";
+ if (modelData.isCoreApp)
+ return "";
+ const desktopEntry = DesktopEntries.heuristicLookup(appItem.appId);
+ return Paths.getAppIcon(appItem.appId, desktopEntry);
+ }
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ visible: status === Image.Ready && !coreIcon.visible
+ layer.enabled: appItem.appId === "org.quickshell" || appItem.appId === "com.danklinux.dms"
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.primary
+ }
+ z: 2
+
+ transformOrigin: Item.Center
+ scale: appItem.enlargeScale
+ Behavior on scale {
+ NumberAnimation {
+ duration: 120
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ DankIcon {
+ readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
+ anchors.left: (root.isVerticalOrientation || isCompact) ? undefined : parent.left
+ anchors.leftMargin: (root.isVerticalOrientation || isCompact) ? 0 : Theme.spacingXS
+ anchors.top: (root.isVerticalOrientation && !isCompact) ? parent.top : undefined
+ anchors.topMargin: (root.isVerticalOrientation && !isCompact) ? Theme.spacingXS : 0
+ anchors.centerIn: (root.isVerticalOrientation || isCompact) ? parent : undefined
+
+ size: appItem.effectiveIconSize
+ name: "sports_esports"
+ color: Theme.widgetTextColor
+ visible: !iconImg.visible && !coreIcon.visible && Paths.isSteamApp(appItem.appId)
+
+ transformOrigin: Item.Center
+ scale: appItem.enlargeScale
+ Behavior on scale {
+ NumberAnimation {
+ duration: 120
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: !iconImg.visible && !coreIcon.visible && !Paths.isSteamApp(appItem.appId)
+ text: {
+ root._desktopEntriesUpdateTrigger;
+ if (!appItem.appId)
+ return "?";
+ const desktopEntry = DesktopEntries.heuristicLookup(appItem.appId);
+ const appName = Paths.getAppName(appItem.appId, desktopEntry);
+ return appName.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+
+ transformOrigin: Item.Center
+ scale: appItem.enlargeScale
+ Behavior on scale {
+ NumberAnimation {
+ duration: 120
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? -2 : 2
+ anchors.bottomMargin: -2
+ width: 14
+ height: 14
+ radius: 7
+ color: Theme.primary
+ visible: modelData.type === "grouped" && appItem.windowCount > 1 && (widgetData?.barShowOverflowBadge !== undefined ? widgetData.barShowOverflowBadge : SettingsData.barShowOverflowBadge)
+ z: 10
+
+ StyledText {
+ anchors.centerIn: parent
+ text: appItem.windowCount > 9 ? "9+" : appItem.windowCount
+ font.pixelSize: 9
+ color: Theme.surface
+ }
+ }
+
+ StyledText {
+ visible: !root.isVerticalOrientation && !(widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
+ anchors.left: iconImg.right
+ anchors.leftMargin: Theme.spacingXS
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ text: appItem.windowTitle || appItem.appId
+ font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ }
+
+ Rectangle {
+ visible: modelData.isRunning && !(widgetData?.appsDockHideIndicators !== undefined ? widgetData.appsDockHideIndicators : SettingsData.appsDockHideIndicators)
+ width: root.isVerticalOrientation ? 2 : 20
+ height: root.isVerticalOrientation ? 20 : 2
+ radius: 1
+ color: appItem.isFocused ? Theme.primary : Theme.surfaceText
+ opacity: appItem.isFocused ? 1 : 0.5
+
+ anchors.bottom: root.isVerticalOrientation ? undefined : parent.bottom
+ anchors.right: root.isVerticalOrientation ? parent.right : undefined
+ anchors.horizontalCenter: root.isVerticalOrientation ? undefined : parent.horizontalCenter
+ anchors.verticalCenter: root.isVerticalOrientation ? parent.verticalCenter : undefined
+
+ anchors.margins: 0
+ z: 5
+ }
+ }
+ }
+
+ // Handler for Drag Logic
+ Item {
+ id: dragHandler
+ anchors.fill: parent
+ property bool dragging: false
+ property point dragStartPos: Qt.point(0, 0)
+ property real dragAxisOffset: 0
+ property bool longPressing: false
+
+ Timer {
+ id: longPressTimer
+ interval: 500
+ repeat: false
+ onTriggered: {
+ if (modelData.isPinned) {
+ dragHandler.longPressing = true;
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+
+ onPressed: mouse => {
+ if (mouse.button === Qt.LeftButton && modelData.isPinned) {
+ dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
+ longPressTimer.start();
+ }
+ }
+
+ onReleased: mouse => {
+ longPressTimer.stop();
+ const wasDragging = dragHandler.dragging;
+ const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
+
+ if (didReorder) {
+ root.movePinnedApp(root.draggedIndex, root.dropTargetIndex);
+ }
+
+ dragHandler.longPressing = false;
+ dragHandler.dragging = false;
+ dragHandler.dragAxisOffset = 0;
+ root.draggedIndex = -1;
+ root.dropTargetIndex = -1;
+
+ if (wasDragging || mouse.button !== Qt.LeftButton)
+ return;
+
+ if (wasDragging || mouse.button !== Qt.LeftButton)
+ return;
+
+ if (modelData.type === "grouped") {
+ if (modelData.windowCount === 0) {
+ if (modelData.isCoreApp && modelData.coreAppData) {
+ AppSearchService.executeCoreApp(modelData.coreAppData);
+ } else {
+ const moddedId = Paths.moddedAppId(modelData.appId);
+ const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
+ if (desktopEntry)
+ SessionService.launchDesktopEntry(desktopEntry);
+ }
+ } else if (modelData.windowCount === 1) {
+ if (modelData.allWindows[0].toplevel)
+ modelData.allWindows[0].toplevel.activate();
+ } else {
+ let currentIndex = -1;
+ for (var i = 0; i < modelData.allWindows.length; i++) {
+ if (modelData.allWindows[i].toplevel.activated) {
+ currentIndex = i;
+ break;
+ }
+ }
+ const nextIndex = (currentIndex + 1) % modelData.allWindows.length;
+ modelData.allWindows[nextIndex].toplevel.activate();
+ }
+ }
+ }
+
+ onPositionChanged: mouse => {
+ if (dragHandler.longPressing && !dragHandler.dragging) {
+ const distance = Math.sqrt(Math.pow(mouse.x - dragHandler.dragStartPos.x, 2) + Math.pow(mouse.y - dragHandler.dragStartPos.y, 2));
+ if (distance > 5) {
+ dragHandler.dragging = true;
+ root.draggedIndex = index;
+ root.dropTargetIndex = index;
+ }
+ }
+
+ if (!dragHandler.dragging)
+ return;
+
+ const axisOffset = root.isVerticalOrientation ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
+ dragHandler.dragAxisOffset = axisOffset;
+
+ const itemSize = (root.isVerticalOrientation ? delegateItem.height : delegateItem.width) + Theme.spacingXS;
+ const slotOffset = Math.round(axisOffset / itemSize);
+ const newTargetIndex = Math.max(0, Math.min(root.pinnedAppCount - 1, index + slotOffset));
+
+ if (newTargetIndex !== root.dropTargetIndex) {
+ root.dropTargetIndex = newTargetIndex;
+ }
+ }
+
+ onEntered: {
+ root.hoveredItem = delegateItem;
+ if (isSeparator || isOverflowToggle)
+ return;
+
+ tooltipLoader.active = true;
+ if (tooltipLoader.item) {
+ if (root.isVerticalOrientation) {
+ const globalPos = delegateItem.mapToGlobal(0, delegateItem.height / 2);
+ const screenX = root.parentScreen ? root.parentScreen.x : 0;
+ const screenY = root.parentScreen ? root.parentScreen.y : 0;
+ const isLeft = root.axis?.edge === "left";
+ const tooltipX = isLeft ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
+ const screenRelativeY = globalPos.y - screenY + root.minTooltipY;
+ tooltipLoader.item.show(appItem.tooltipText, screenX + tooltipX, screenRelativeY, root.parentScreen, isLeft, !isLeft);
+ } else {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
+ const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
+ const isBottom = root.axis?.edge === "bottom";
+ const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
+ tooltipLoader.item.show(appItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
+ }
+ }
+ }
+ onExited: {
+ if (root.hoveredItem === delegateItem) {
+ root.hoveredItem = null;
+ if (tooltipLoader.item)
+ tooltipLoader.item.hide();
+ tooltipLoader.active = false;
+ }
+ }
+
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ if (tooltipLoader.item) {
+ tooltipLoader.item.hide();
+ }
+ tooltipLoader.active = false;
+ contextMenuLoader.active = true;
+
+ if (contextMenuLoader.item) {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
+ const screenX = root.parentScreen ? root.parentScreen.x : 0;
+ const screenY = root.parentScreen ? root.parentScreen.y : 0;
+ const isBarVertical = root.axis?.isVertical ?? false;
+ const barEdge = root.axis?.edge ?? "top";
+
+ let x = globalPos.x - screenX;
+ let y = globalPos.y - screenY;
+
+ switch (barEdge) {
+ case "bottom":
+ y = (root.parentScreen ? root.parentScreen.height : Screen.height) - root.barThickness - root.barSpacing;
+ break;
+ case "top":
+ y = root.barThickness + root.barSpacing;
+ break;
+ case "left":
+ x = root.barThickness + root.barSpacing;
+ break;
+ case "right":
+ x = (root.parentScreen ? root.parentScreen.width : Screen.width) - root.barThickness - root.barSpacing;
+ break;
+ }
+
+ const shouldHidePin = modelData.appId === "org.quickshell" || modelData.appId === "com.danklinux.dms";
+ const moddedId = Paths.moddedAppId(modelData.appId);
+ const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
+
+ contextMenuLoader.item.showAt(x, y, isBarVertical, barEdge, modelData, shouldHidePin, desktopEntry, root.parentScreen);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: contextMenuLoader
+ active: false
+ source: "AppsDockContextMenu.qml"
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml
new file mode 100644
index 0000000..46c4f69
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml
@@ -0,0 +1,486 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Widgets
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+PanelWindow {
+ id: root
+
+ WlrLayershell.namespace: "dms:dock-context-menu"
+
+ property var appData: null
+ property var anchorItem: null
+ property int margin: 10
+ property bool hidePin: false
+ property var desktopEntry: null
+ property bool isDmsWindow: appData?.appId === "org.quickshell" || appData?.appId === "com.danklinux.dms"
+
+ property bool isVertical: false
+ property string edge: "top"
+ property point anchorPos: Qt.point(0, 0)
+
+ function showAt(x, y, vertical, barEdge, data, hidePinOption, entry, targetScreen) {
+ if (targetScreen) {
+ root.screen = targetScreen;
+ }
+
+ anchorPos = Qt.point(x, y);
+ isVertical = vertical ?? false;
+ edge = barEdge ?? "top";
+
+ appData = data;
+ hidePin = hidePinOption || false;
+ desktopEntry = entry || null;
+
+ visible = true;
+
+ if (targetScreen) {
+ TrayMenuManager.registerMenu(targetScreen.name, root);
+ }
+ }
+
+ function close() {
+ visible = false;
+
+ if (root.screen) {
+ TrayMenuManager.unregisterMenu(root.screen.name);
+ }
+ }
+
+ screen: null
+ visible: false
+ WlrLayershell.layer: WlrLayershell.Overlay
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+ color: "transparent"
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ Component.onDestruction: {
+ if (root.screen) {
+ TrayMenuManager.unregisterMenu(root.screen.name);
+ }
+ }
+
+ Connections {
+ target: PopoutManager
+ function onPopoutOpening() {
+ root.close();
+ }
+ }
+
+ Rectangle {
+ id: menuContainer
+
+ x: {
+ if (root.isVertical) {
+ if (root.edge === "left") {
+ return Math.min(root.width - width - 10, root.anchorPos.x);
+ } else {
+ return Math.max(10, root.anchorPos.x - width);
+ }
+ } else {
+ const left = 10;
+ const right = root.width - width - 10;
+ const want = root.anchorPos.x - width / 2;
+ return Math.max(left, Math.min(right, want));
+ }
+ }
+ y: {
+ if (root.isVertical) {
+ const top = 10;
+ const bottom = root.height - height - 10;
+ const want = root.anchorPos.y - height / 2;
+ return Math.max(top, Math.min(bottom, want));
+ } else {
+ if (root.edge === "top") {
+ return Math.min(root.height - height - 10, root.anchorPos.y);
+ } else {
+ return Math.max(10, root.anchorPos.y - height);
+ }
+ }
+ }
+
+ width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2))
+ height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: 1
+
+ opacity: root.visible ? 1 : 0
+ visible: opacity > 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ 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: -1
+ }
+
+ Column {
+ id: menuColumn
+ width: parent.width - Theme.spacingS * 2
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: Theme.spacingS
+ spacing: 1
+
+ // Window list for grouped apps
+ Repeater {
+ model: {
+ if (!root.appData || root.appData.type !== "grouped")
+ return [];
+
+ const toplevels = [];
+ const allToplevels = ToplevelManager.toplevels.values;
+ for (let i = 0; i < allToplevels.length; i++) {
+ const toplevel = allToplevels[i];
+ if (toplevel.appId === root.appData.appId) {
+ toplevels.push(toplevel);
+ }
+ }
+ return toplevels;
+ }
+
+ Rectangle {
+ width: parent.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: closeButton.left
+ anchors.rightMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ text: (modelData && modelData.title) ? modelData.title : I18n.tr("(Unnamed)")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Normal
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ }
+
+ Rectangle {
+ id: closeButton
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ width: 20
+ height: 20
+ radius: 10
+ color: closeMouseArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "close"
+ size: 12
+ color: closeMouseArea.containsMouse ? Theme.error : Theme.surfaceText
+ }
+
+ MouseArea {
+ id: closeMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (modelData && modelData.close) {
+ modelData.close();
+ }
+ root.close();
+ }
+ }
+ }
+
+ DankRipple {
+ id: windowRipple
+ rippleColor: Theme.surfaceText
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: windowArea
+ anchors.fill: parent
+ anchors.rightMargin: 24
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => windowRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (modelData && modelData.activate) {
+ modelData.activate();
+ }
+ root.close();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ visible: {
+ if (!root.appData)
+ return false;
+ if (root.appData.type !== "grouped")
+ return false;
+ return root.appData.windowCount > 0;
+ }
+ width: parent.width
+ height: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ }
+
+ Repeater {
+ model: root.desktopEntry && root.desktopEntry.actions ? root.desktopEntry.actions : []
+
+ Rectangle {
+ width: parent.width
+ height: 28
+ radius: Theme.cornerRadius
+ color: actionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ Item {
+ anchors.verticalCenter: parent.verticalCenter
+ width: 16
+ height: 16
+ visible: modelData.icon && modelData.icon !== ""
+
+ IconImage {
+ anchors.fill: parent
+ source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : ""
+ smooth: true
+ asynchronous: true
+ visible: status === Image.Ready
+ }
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelData.name || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Normal
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ }
+ }
+
+ DankRipple {
+ id: actionRipple
+ rippleColor: Theme.surfaceText
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: actionArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => actionRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (modelData) {
+ SessionService.launchDesktopAction(root.desktopEntry, modelData);
+ }
+ root.close();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ visible: {
+ if (!root.desktopEntry?.actions || root.desktopEntry.actions.length === 0) {
+ return false;
+ }
+ return !root.hidePin || (!root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand);
+ }
+ width: parent.width
+ height: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ }
+
+ Rectangle {
+ visible: !root.hidePin
+ width: parent.width
+ height: 28
+ radius: Theme.cornerRadius
+ color: pinArea.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: root.appData && root.appData.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Normal
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ }
+
+ DankRipple {
+ id: pinRipple
+ rippleColor: Theme.surfaceText
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: pinArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (!root.appData) {
+ return;
+ }
+ if (root.appData.isPinned) {
+ SessionData.removeBarPinnedApp(root.appData.appId);
+ } else {
+ SessionData.addBarPinnedApp(root.appData.appId);
+ }
+ root.close();
+ }
+ }
+ }
+
+ Rectangle {
+ visible: {
+ const hasNvidia = !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand;
+ const hasWindow = root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0));
+ const hasPinOption = !root.hidePin;
+ const hasContentAbove = hasPinOption || hasNvidia;
+ return hasContentAbove && hasWindow;
+ }
+ width: parent.width
+ height: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ }
+
+ Rectangle {
+ visible: !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand
+ width: parent.width
+ height: 28
+ radius: Theme.cornerRadius
+ color: nvidiaArea.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: I18n.tr("Launch on dGPU")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Normal
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ }
+
+ DankRipple {
+ id: nvidiaRipple
+ rippleColor: Theme.surfaceText
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: nvidiaArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => nvidiaRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (root.desktopEntry) {
+ SessionService.launchDesktopEntry(root.desktopEntry, true);
+ }
+ root.close();
+ }
+ }
+ }
+
+ Rectangle {
+ visible: root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0))
+ width: parent.width
+ height: 28
+ radius: Theme.cornerRadius
+ color: closeArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.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: {
+ if (root.appData && root.appData.type === "grouped") {
+ return I18n.tr("Close All Windows");
+ }
+ return I18n.tr("Close Window");
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
+ font.weight: Font.Normal
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ }
+
+ DankRipple {
+ id: closeRipple
+ rippleColor: Theme.error
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: closeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => closeRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (root.appData?.type === "window") {
+ root.appData?.toplevel?.close();
+ } else if (root.appData?.type === "grouped") {
+ root.appData?.allWindows?.forEach(window => window.toplevel?.close());
+ }
+ root.close();
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ z: -1
+ onClicked: root.close()
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockOverflowButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockOverflowButton.qml
new file mode 100644
index 0000000..82c78d0
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockOverflowButton.qml
@@ -0,0 +1,76 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: root
+
+ property real iconSize: 24
+ property int overflowCount: 0
+ property bool overflowExpanded: false
+ property bool isVertical: false
+ property bool showBadge: true
+
+ signal clicked
+
+ Rectangle {
+ id: buttonBackground
+ anchors.centerIn: parent
+ width: root.iconSize
+ height: root.iconSize
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, mouseArea.containsMouse ? 0.2 : 0.1)
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+
+ DankIcon {
+ id: arrowIcon
+ anchors.centerIn: parent
+ size: root.iconSize * 0.6
+ name: "expand_more"
+ color: Theme.widgetIconColor
+ rotation: isVertical ? (overflowExpanded ? 180 : 0) : (overflowExpanded ? 90 : -90)
+
+ Behavior on rotation {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ visible: overflowCount > 0 && !overflowExpanded && root.showBadge
+ anchors.right: buttonBackground.right
+ anchors.top: buttonBackground.top
+ anchors.rightMargin: -4
+ anchors.topMargin: -4
+ width: Math.max(18, badgeText.width + 8)
+ height: 18
+ radius: 9
+ color: Theme.primary
+ z: 10
+
+ StyledText {
+ id: badgeText
+ anchors.centerIn: parent
+ text: `+${overflowCount}`
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Bold
+ color: Theme.onPrimary
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml
new file mode 100644
index 0000000..52ace33
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml
@@ -0,0 +1,94 @@
+import QtQuick
+import Quickshell.Services.Mpris
+import qs.Common
+import qs.Services
+
+Item {
+ id: root
+
+ readonly property MprisPlayer activePlayer: MprisController.activePlayer
+ readonly property bool hasActiveMedia: activePlayer !== null
+ readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
+
+ width: 20
+ height: Theme.iconSize
+
+ Loader {
+ active: isPlaying
+
+ sourceComponent: Component {
+ Ref {
+ service: CavaService
+ }
+ }
+ }
+
+ readonly property real maxBarHeight: Theme.iconSize - 2
+ readonly property real minBarHeight: 3
+ readonly property real heightRange: maxBarHeight - minBarHeight
+ property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight]
+
+ Timer {
+ id: fallbackTimer
+
+ running: !CavaService.cavaAvailable && isPlaying
+ interval: 500
+ repeat: true
+ onTriggered: {
+ CavaService.values = [Math.random() * 20 + 5, Math.random() * 25 + 8, Math.random() * 22 + 6, Math.random() * 20 + 5, Math.random() * 22 + 6, Math.random() * 25 + 8];
+ }
+ }
+
+ Connections {
+ target: CavaService
+ function onValuesChanged() {
+ if (!root.isPlaying) {
+ root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight];
+ return;
+ }
+
+ const newHeights = [];
+ for (let i = 0; i < 6; i++) {
+ if (CavaService.values.length <= i) {
+ newHeights.push(root.minBarHeight);
+ continue;
+ }
+
+ const rawLevel = CavaService.values[i];
+ if (rawLevel <= 0) {
+ newHeights.push(root.minBarHeight);
+ } else if (rawLevel >= 100) {
+ newHeights.push(root.maxBarHeight);
+ } else {
+ newHeights.push(root.minBarHeight + Math.sqrt(rawLevel * 0.01) * root.heightRange);
+ }
+ }
+ root.barHeights = newHeights;
+ }
+ }
+
+ Row {
+ anchors.centerIn: parent
+ spacing: 1.5
+
+ Repeater {
+ model: 6
+
+ Rectangle {
+ width: 2
+ height: root.barHeights[index]
+ radius: 1.5
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+
+ Behavior on height {
+ enabled: root.isPlaying && !CavaService.cavaAvailable
+ NumberAnimation {
+ duration: 100
+ easing.type: Easing.Linear
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml
new file mode 100644
index 0000000..51d4b54
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml
@@ -0,0 +1,165 @@
+import QtQuick
+import Quickshell.Services.UPower
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: battery
+
+ property bool batteryPopupVisible: false
+ property var popoutTarget: null
+
+ property real touchpadAccumulator: 0
+
+ readonly property int barPosition: {
+ switch (axis?.edge) {
+ case "top":
+ return 0;
+ case "bottom":
+ return 1;
+ case "left":
+ return 2;
+ case "right":
+ return 3;
+ default:
+ return 0;
+ }
+ }
+
+ signal toggleBatteryPopup
+
+ visible: true
+
+ content: Component {
+ Item {
+ implicitWidth: battery.isVerticalOrientation ? (battery.widgetThickness - battery.horizontalPadding * 2) : batteryContent.implicitWidth
+ implicitHeight: battery.isVerticalOrientation ? batteryColumn.implicitHeight : (battery.widgetThickness - battery.horizontalPadding * 2)
+
+ Column {
+ id: batteryColumn
+ visible: battery.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: BatteryService.getBatteryIcon()
+ size: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (!BatteryService.batteryAvailable) {
+ return Theme.widgetIconColor;
+ }
+
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+
+ if (BatteryService.isCharging || BatteryService.isPluggedIn) {
+ return Theme.primary;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: BatteryService.batteryLevel.toString()
+ font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale, battery.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: BatteryService.batteryAvailable
+ }
+ }
+
+ Row {
+ id: batteryContent
+ visible: !battery.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: (barConfig?.noBackground ?? false) ? 1 : 2
+
+ DankIcon {
+ name: BatteryService.getBatteryIcon()
+ size: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (!BatteryService.batteryAvailable) {
+ return Theme.widgetIconColor;
+ }
+
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+
+ if (BatteryService.isCharging || BatteryService.isPluggedIn) {
+ return Theme.primary;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: `${BatteryService.batteryLevel}%`
+ font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale, battery.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ visible: BatteryService.batteryAvailable
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ x: -battery.leftMargin
+ y: -battery.topMargin
+ width: battery.width + battery.leftMargin + battery.rightMargin
+ height: battery.height + battery.topMargin + battery.bottomMargin
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+ onPressed: mouse => {
+ battery.triggerRipple(this, mouse.x, mouse.y);
+ toggleBatteryPopup();
+ }
+ onWheel: wheel => {
+ var delta = wheel.angleDelta.y;
+ if (delta === 0)
+ return;
+
+ // Check if this is a touchpad
+ if (delta !== 120 && delta !== -120) {
+ touchpadAccumulator += delta;
+ console.info("Acc: "+touchpadAccumulator);
+ if (Math.abs(touchpadAccumulator) < 500)
+ return;
+ delta = touchpadAccumulator;
+ touchpadAccumulator = 0;
+ }
+ console.info("Trigger! Delta: "+delta)
+
+ // This is after the other delta checks so it only shows on valid Y scroll
+ if (typeof PowerProfiles === "undefined") {
+ ToastService.showError("power-profiles-daemon not available");
+ return;
+ }
+
+ // Get list of profiles, and current index
+ const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []);
+ var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
+
+ // Step once based on mouse wheel direction
+ if (delta > 0) index += 1;
+ else index -= 1;
+
+ // Already at end of list, can't go further
+ if (index < 0 || index >= profiles.length) return;
+
+ // Set new profile
+ PowerProfiles.profile = profiles[index];
+ if (PowerProfiles.profile !== profiles[index]) {
+ ToastService.showError("Failed to set power profile");
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CapsLockIndicator.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CapsLockIndicator.qml
new file mode 100644
index 0000000..ada41dd
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CapsLockIndicator.qml
@@ -0,0 +1,62 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ opacity: DMSService.capsLockState ? 1 : 0
+
+ states: [
+ State {
+ name: "hidden_horizontal"
+ when: !DMSService.capsLockState && !isVerticalOrientation
+ PropertyChanges {
+ target: root
+ width: 0
+ }
+ },
+ State {
+ name: "hidden_vertical"
+ when: !DMSService.capsLockState && isVerticalOrientation
+ PropertyChanges {
+ target: root
+ height: 0
+ }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ NumberAnimation {
+ properties: "width,height"
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ ]
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: icon.width
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ id: icon
+ anchors.centerIn: parent
+ name: "shift_lock"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.primary
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ClipboardButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ClipboardButton.qml
new file mode 100644
index 0000000..e970662
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ClipboardButton.qml
@@ -0,0 +1,314 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool isActive: false
+ property bool isAutoHideBar: false
+
+ signal clipboardClicked
+ signal showSavedItemsRequested
+ signal clearAllRequested
+
+ readonly property real minTooltipY: {
+ if (!parentScreen || !(axis?.isVertical ?? false)) {
+ return 0;
+ }
+ if (isAutoHideBar) {
+ return 0;
+ }
+ if (parentScreen.y > 0) {
+ return barThickness + barSpacing;
+ }
+ return 0;
+ }
+
+ function clamp(value, minValue, maxValue) {
+ return Math.max(minValue, Math.min(maxValue, value));
+ }
+
+ function openContextMenu() {
+ const screen = root.parentScreen || Screen;
+ const screenX = screen.x || 0;
+ const screenY = screen.y || 0;
+ const isVertical = root.axis?.isVertical ?? false;
+ const edge = root.axis?.edge ?? "top";
+ const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS);
+
+ const globalPos = root.mapToGlobal(root.width / 2, root.height / 2);
+ const relativeX = globalPos.x - screenX;
+ const relativeY = globalPos.y - screenY;
+
+ let anchorX = relativeX;
+ let anchorY = relativeY;
+
+ if (isVertical) {
+ anchorX = edge === "left" ? (root.barThickness + root.barSpacing + gap) : (screen.width - (root.barThickness + root.barSpacing + gap));
+ anchorY = relativeY + root.minTooltipY;
+ } else {
+ anchorX = relativeX;
+ anchorY = edge === "bottom" ? (screen.height - (root.barThickness + root.barSpacing + gap)) : (root.barThickness + root.barSpacing + gap);
+ }
+
+ contextMenuWindow.showAt(anchorX, anchorY, isVertical, edge, screen);
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => root.triggerRipple(this, mouse.x, mouse.y)
+ onClicked: function (mouse) {
+ switch (mouse.button) {
+ case Qt.RightButton:
+ openContextMenu();
+ break;
+ case Qt.LeftButton:
+ clipboardClicked();
+ break;
+ }
+ }
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: icon.width
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ id: icon
+ anchors.centerIn: parent
+ name: "content_paste"
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetIconColor
+ }
+ }
+ }
+
+ PanelWindow {
+ id: contextMenuWindow
+
+ WindowBlur {
+ targetWindow: contextMenuWindow
+ blurX: menuContainer.x
+ blurY: menuContainer.y
+ blurWidth: contextMenuWindow.visible ? menuContainer.width : 0
+ blurHeight: contextMenuWindow.visible ? menuContainer.height : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ WlrLayershell.namespace: "dms:clipboard-context-menu"
+
+ property bool isVertical: false
+ property string edge: "top"
+ property point anchorPos: Qt.point(0, 0)
+
+ function showAt(x, y, vertical, barEdge, targetScreen) {
+ if (targetScreen) {
+ contextMenuWindow.screen = targetScreen;
+ }
+
+ anchorPos = Qt.point(x, y);
+ isVertical = vertical ?? false;
+ edge = barEdge ?? "top";
+
+ visible = true;
+
+ if (contextMenuWindow.screen) {
+ TrayMenuManager.registerMenu(contextMenuWindow.screen.name, contextMenuWindow);
+ }
+ }
+
+ function closeMenu() {
+ visible = false;
+
+ if (contextMenuWindow.screen) {
+ TrayMenuManager.unregisterMenu(contextMenuWindow.screen.name);
+ }
+ }
+
+ screen: null
+ visible: false
+ WlrLayershell.layer: WlrLayershell.Overlay
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+ color: "transparent"
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ Component.onDestruction: {
+ if (contextMenuWindow.screen) {
+ TrayMenuManager.unregisterMenu(contextMenuWindow.screen.name);
+ }
+ }
+
+ Connections {
+ target: PopoutManager
+ function onPopoutOpening() {
+ contextMenuWindow.closeMenu();
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onClicked: contextMenuWindow.closeMenu()
+ }
+
+ Rectangle {
+ id: menuContainer
+
+ x: {
+ if (contextMenuWindow.isVertical) {
+ if (contextMenuWindow.edge === "left") {
+ return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x);
+ }
+ return Math.max(10, contextMenuWindow.anchorPos.x - width);
+ }
+ const left = 10;
+ const right = contextMenuWindow.width - width - 10;
+ const want = contextMenuWindow.anchorPos.x - width / 2;
+ return Math.max(left, Math.min(right, want));
+ }
+ y: {
+ if (contextMenuWindow.isVertical) {
+ const top = 10;
+ const bottom = contextMenuWindow.height - height - 10;
+ const want = contextMenuWindow.anchorPos.y - height / 2;
+ return Math.max(top, Math.min(bottom, want));
+ }
+ if (contextMenuWindow.edge === "top") {
+ return Math.min(contextMenuWindow.height - height - 10, contextMenuWindow.anchorPos.y);
+ }
+ return Math.max(10, contextMenuWindow.anchorPos.y - height);
+ }
+
+ width: Math.min(240, Math.max(170, menuColumn.implicitWidth + Theme.spacingS * 2))
+ height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: BlurService.enabled ? BlurService.borderWidth : 1
+
+ opacity: contextMenuWindow.visible ? 1 : 0
+ visible: opacity > 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ 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: -1
+ }
+
+ Column {
+ id: menuColumn
+ width: parent.width - Theme.spacingS * 2
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: Theme.spacingS
+ spacing: 1
+
+ Rectangle {
+ id: clearAllItem
+ width: parent.width
+ height: 30
+ radius: Theme.cornerRadius
+ color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: "delete_sweep"
+ size: 16
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: I18n.tr("Clear All")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: clearAllArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ contextMenuWindow.closeMenu();
+ root.clearAllRequested();
+ }
+ }
+ }
+
+ Rectangle {
+ id: savedItemsItem
+ width: parent.width
+ height: 30
+ radius: Theme.cornerRadius
+ color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: "push_pin"
+ size: 16
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: I18n.tr("Show Saved Items")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: savedItemsArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ contextMenuWindow.closeMenu();
+ root.showSavedItemsRequested();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Clock.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Clock.qml
new file mode 100644
index 0000000..c49221e
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Clock.qml
@@ -0,0 +1,331 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Modules.Plugins
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property var widgetData: null
+ property bool compactMode: false
+ signal clockClicked
+
+ onClicked: clockClicked()
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : clockRow.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? clockColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
+
+ readonly property bool compact: widgetData?.clockCompactMode !== undefined ? widgetData.clockCompactMode : SettingsData.clockCompactMode
+
+ Column {
+ id: clockColumn
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 0
+
+ Row {
+ spacing: 0
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: {
+ const hours = systemClock?.date?.getHours();
+ if (SettingsData.use24HourClock)
+ return String(hours).padStart(2, '0').charAt(0);
+ const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
+ return String(display).padStart(2, '0').charAt(0);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+
+ StyledText {
+ text: {
+ const hours = systemClock?.date?.getHours();
+ if (SettingsData.use24HourClock)
+ return String(hours).padStart(2, '0').charAt(1);
+ const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
+ return String(display).padStart(2, '0').charAt(1);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+ }
+
+ Row {
+ spacing: 0
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+
+ StyledText {
+ text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+ }
+
+ Row {
+ visible: SettingsData.showSeconds
+ spacing: 0
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+
+ StyledText {
+ text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: Theme.spacingM
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: !compact
+
+ Rectangle {
+ width: parent.width * 0.6
+ height: 1
+ color: Theme.outlineButton
+ anchors.centerIn: parent
+ }
+ }
+
+ Row {
+ spacing: 0
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: !compact
+
+ StyledText {
+ text: {
+ const locale = I18n.locale();
+ const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
+ const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
+ const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
+ return value.charAt(0);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.primary
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+
+ StyledText {
+ text: {
+ const locale = I18n.locale();
+ const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
+ const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
+ const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
+ return value.charAt(1);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.primary
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+ }
+
+ Row {
+ spacing: 0
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: !compact
+
+ StyledText {
+ text: {
+ const locale = I18n.locale();
+ const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
+ const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
+ const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
+ return value.charAt(0);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.primary
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+
+ StyledText {
+ text: {
+ const locale = I18n.locale();
+ const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
+ const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
+ const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
+ return value.charAt(1);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.primary
+ width: Math.round(font.pixelSize * 0.6)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignBottom
+ }
+ }
+ }
+
+ Row {
+ id: clockRow
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ property real fontSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ property real digitWidth: fontSize * 0.6
+
+ property string hoursStr: {
+ const hours = systemClock?.date?.getHours() ?? 0;
+ if (SettingsData.use24HourClock)
+ return String(hours).padStart(2, '0');
+ const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
+ if (SettingsData.padHours12Hour)
+ return String(display).padStart(2, '0');
+ return String(display);
+ }
+ property string minutesStr: String(systemClock?.date?.getMinutes() ?? 0).padStart(2, '0')
+ property string secondsStr: String(systemClock?.date?.getSeconds() ?? 0).padStart(2, '0')
+ property string ampmStr: {
+ if (SettingsData.use24HourClock)
+ return "";
+ const hours = systemClock?.date?.getHours() ?? 0;
+ return hours >= 12 ? " PM" : " AM";
+ }
+
+ Row {
+ spacing: 0
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ visible: clockRow.hoursStr.length > 1
+ text: clockRow.hoursStr.charAt(0)
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ width: clockRow.digitWidth
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: clockRow.hoursStr.length > 1 ? clockRow.hoursStr.charAt(1) : clockRow.hoursStr.charAt(0)
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ width: clockRow.digitWidth
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: ":"
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ }
+
+ StyledText {
+ text: clockRow.minutesStr.charAt(0)
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ width: clockRow.digitWidth
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: clockRow.minutesStr.charAt(1)
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ width: clockRow.digitWidth
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ visible: SettingsData.showSeconds
+ text: ":"
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ }
+
+ StyledText {
+ visible: SettingsData.showSeconds
+ text: clockRow.secondsStr.charAt(0)
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ width: clockRow.digitWidth
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ visible: SettingsData.showSeconds
+ text: clockRow.secondsStr.charAt(1)
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ width: clockRow.digitWidth
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ visible: !SettingsData.use24HourClock
+ text: clockRow.ampmStr
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ }
+ }
+
+ StyledText {
+ id: middleDot
+ text: "•"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.outlineButton
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !compact
+ }
+
+ StyledText {
+ id: dateText
+ text: {
+ if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
+ return systemClock?.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat);
+ }
+ return systemClock?.date?.toLocaleDateString(I18n.locale(), "ddd d");
+ }
+ font.pixelSize: clockRow.fontSize
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !compact
+ }
+ }
+
+ SystemClock {
+ id: systemClock
+ precision: SettingsData.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ColorPicker.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ColorPicker.qml
new file mode 100644
index 0000000..d3330cd
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ColorPicker.qml
@@ -0,0 +1,37 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool isActive: false
+
+ signal colorPickerRequested
+
+ content: Component {
+ Item {
+ implicitWidth: icon.width
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ id: icon
+ anchors.centerIn: parent
+ name: "palette"
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: root.isActive ? Theme.primary : Theme.surfaceText
+ }
+ }
+ }
+
+ MouseArea {
+ z: 1
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ root.colorPickerRequested();
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml
new file mode 100644
index 0000000..5413d20
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml
@@ -0,0 +1,830 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool isActive: false
+ property var popoutTarget: null
+ property var widgetData: null
+ property string screenName: ""
+ property string screenModel: ""
+ property bool showNetworkIcon: widgetData?.showNetworkIcon !== undefined ? widgetData.showNetworkIcon : SettingsData.controlCenterShowNetworkIcon
+ property bool showBluetoothIcon: widgetData?.showBluetoothIcon !== undefined ? widgetData.showBluetoothIcon : SettingsData.controlCenterShowBluetoothIcon
+ property bool showAudioIcon: widgetData?.showAudioIcon !== undefined ? widgetData.showAudioIcon : SettingsData.controlCenterShowAudioIcon
+ property bool showAudioPercent: widgetData?.showAudioPercent !== undefined ? widgetData.showAudioPercent : SettingsData.controlCenterShowAudioPercent
+ property bool showVpnIcon: widgetData?.showVpnIcon !== undefined ? widgetData.showVpnIcon : SettingsData.controlCenterShowVpnIcon
+ property bool showBrightnessIcon: widgetData?.showBrightnessIcon !== undefined ? widgetData.showBrightnessIcon : SettingsData.controlCenterShowBrightnessIcon
+ property bool showBrightnessPercent: widgetData?.showBrightnessPercent !== undefined ? widgetData.showBrightnessPercent : SettingsData.controlCenterShowBrightnessPercent
+ property bool showMicIcon: widgetData?.showMicIcon !== undefined ? widgetData.showMicIcon : SettingsData.controlCenterShowMicIcon
+ property bool showMicPercent: widgetData?.showMicPercent !== undefined ? widgetData.showMicPercent : SettingsData.controlCenterShowMicPercent
+ property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon
+ property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon
+ property bool showScreenSharingIcon: widgetData?.showScreenSharingIcon !== undefined ? widgetData.showScreenSharingIcon : SettingsData.controlCenterShowScreenSharingIcon
+ property real touchpadThreshold: 100
+ property real micAccumulator: 0
+ property real volumeAccumulator: 0
+ property real brightnessAccumulator: 0
+ readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ property var _hRow: null
+ property var _vCol: null
+ property var _hAudio: null
+ property var _hBrightness: null
+ property var _hMic: null
+ property var _vAudio: null
+ property var _vBrightness: null
+ property var _vMic: null
+ property var _interactionDelegates: []
+ readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]
+ readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder()
+ readonly property var controlCenterRenderModel: getControlCenterRenderModel()
+
+ onIsVerticalOrientationChanged: root.clearInteractionRefs()
+
+ onWheel: function (wheelEvent) {
+ const delta = wheelEvent.angleDelta.y;
+ if (delta === 0)
+ return;
+
+ root.refreshInteractionRefs();
+
+ const rootX = wheelEvent.x - root.leftMargin;
+ const rootY = wheelEvent.y - root.topMargin;
+
+ if (root.isVerticalOrientation && _vCol) {
+ const pos = root.mapToItem(_vCol, rootX, rootY);
+ if (_vBrightness?.visible && pos.y >= _vBrightness.y && pos.y < _vBrightness.y + _vBrightness.height) {
+ root.handleBrightnessWheel(delta);
+ } else if (_vMic?.visible && pos.y >= _vMic.y && pos.y < _vMic.y + _vMic.height) {
+ root.handleMicWheel(delta);
+ } else {
+ root.handleVolumeWheel(delta);
+ }
+ } else if (_hRow) {
+ const pos = root.mapToItem(_hRow, rootX, rootY);
+ if (_hBrightness?.visible && pos.x >= _hBrightness.x && pos.x < _hBrightness.x + _hBrightness.width) {
+ root.handleBrightnessWheel(delta);
+ } else if (_hMic?.visible && pos.x >= _hMic.x && pos.x < _hMic.x + _hMic.width) {
+ root.handleMicWheel(delta);
+ } else {
+ root.handleVolumeWheel(delta);
+ }
+ } else {
+ root.handleVolumeWheel(delta);
+ }
+ wheelEvent.accepted = true;
+ }
+
+ onRightClicked: function (rootX, rootY) {
+ root.refreshInteractionRefs();
+
+ if (root.isVerticalOrientation && _vCol) {
+ const pos = root.mapToItem(_vCol, rootX, rootY);
+ if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
+ AudioService.toggleMute();
+ return;
+ }
+ if (_vMic?.visible && pos.y >= _vMic.y && pos.y < _vMic.y + _vMic.height) {
+ AudioService.toggleMicMute();
+ return;
+ }
+ } else if (_hRow) {
+ const pos = root.mapToItem(_hRow, rootX, rootY);
+ if (_hAudio?.visible && pos.x >= _hAudio.x && pos.x < _hAudio.x + _hAudio.width) {
+ AudioService.toggleMute();
+ return;
+ }
+ if (_hMic?.visible && pos.x >= _hMic.x && pos.x < _hMic.x + _hMic.width) {
+ AudioService.toggleMicMute();
+ return;
+ }
+ }
+ }
+
+ Loader {
+ active: root.showPrinterIcon
+ sourceComponent: Component {
+ Ref {
+ service: CupsService
+ }
+ }
+ }
+
+ function getNetworkIconName() {
+ if (NetworkService.wifiToggling)
+ return "sync";
+ switch (NetworkService.networkStatus) {
+ case "ethernet":
+ return "lan";
+ case "vpn":
+ return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon;
+ default:
+ return NetworkService.wifiSignalIcon;
+ }
+ }
+
+ function getNetworkIconColor() {
+ if (NetworkService.wifiToggling)
+ return Theme.primary;
+ return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText;
+ }
+
+ function getVolumeIconName() {
+ if (!AudioService.sink?.audio)
+ return "volume_up";
+ if (AudioService.sink.audio.muted)
+ return "volume_off";
+ if (AudioService.sink.audio.volume === 0)
+ return "volume_mute";
+ if (AudioService.sink.audio.volume * 100 < 33)
+ return "volume_down";
+ return "volume_up";
+ }
+
+ function getMicIconName() {
+ if (!AudioService.source?.audio)
+ return "mic";
+ if (AudioService.source.audio.muted || AudioService.source.audio.volume === 0)
+ return "mic_off";
+ return "mic";
+ }
+
+ function getMicIconColor() {
+ if (!AudioService.source?.audio)
+ return Theme.surfaceText;
+ if (AudioService.source.audio.muted || AudioService.source.audio.volume === 0)
+ return Theme.surfaceText;
+ return Theme.widgetIconColor;
+ }
+
+ function getBrightnessIconName() {
+ const deviceName = getPinnedBrightnessDevice();
+ if (!deviceName)
+ return "brightness_medium";
+ const level = DisplayService.getDeviceBrightness(deviceName);
+ if (level <= 33)
+ return "brightness_low";
+ if (level <= 66)
+ return "brightness_medium";
+ return "brightness_high";
+ }
+
+ function getScreenPinKey() {
+ if (!root.screenName)
+ return "";
+ const screen = Quickshell.screens.find(s => s.name === root.screenName);
+ if (screen) {
+ return SettingsData.getScreenDisplayName(screen);
+ }
+ if (SettingsData.displayNameMode === "model" && root.screenModel && root.screenModel.length > 0) {
+ return root.screenModel;
+ }
+ return root.screenName;
+ }
+
+ function getPinnedBrightnessDevice() {
+ const pinKey = getScreenPinKey();
+ if (!pinKey)
+ return "";
+ const pins = SettingsData.brightnessDevicePins || {};
+ return pins[pinKey] || "";
+ }
+
+ function hasPinnedBrightnessDevice() {
+ return getPinnedBrightnessDevice().length > 0;
+ }
+
+ function handleVolumeWheel(delta) {
+ if (!AudioService.sink?.audio)
+ return;
+
+ var step = 5;
+ const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0;
+ if (!isMouseWheel) {
+ step = 1;
+ volumeAccumulator += delta;
+ if (Math.abs(volumeAccumulator) < touchpadThreshold)
+ return;
+
+ delta = volumeAccumulator;
+ volumeAccumulator = 0;
+ }
+
+ const maxVol = AudioService.sinkMaxVolume;
+ const currentVolume = AudioService.sink.audio.volume * 100;
+ const newVolume = delta > 0 ? Math.min(maxVol, currentVolume + step) : Math.max(0, currentVolume - step);
+ AudioService.sink.audio.muted = false;
+ AudioService.sink.audio.volume = newVolume / 100;
+ AudioService.playVolumeChangeSoundIfEnabled();
+ }
+
+ function handleMicWheel(delta) {
+ if (!AudioService.source?.audio)
+ return;
+
+ var step = 5;
+ const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0;
+ if (!isMouseWheel) {
+ step = 1;
+ micAccumulator += delta;
+ if (Math.abs(micAccumulator) < touchpadThreshold)
+ return;
+
+ delta = micAccumulator;
+ micAccumulator = 0;
+ }
+
+ const currentVolume = AudioService.source.audio.volume * 100;
+ const newVolume = delta > 0 ? Math.min(100, currentVolume + step) : Math.max(0, currentVolume - step);
+ AudioService.source.audio.muted = false;
+ AudioService.source.audio.volume = newVolume / 100;
+ }
+
+ function handleBrightnessWheel(delta) {
+ const deviceName = getPinnedBrightnessDevice();
+ if (!deviceName) {
+ return;
+ }
+
+ var step = 5;
+ const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0;
+ if (!isMouseWheel) {
+ step = 1;
+ brightnessAccumulator += delta;
+ if (Math.abs(brightnessAccumulator) < touchpadThreshold)
+ return;
+
+ delta = brightnessAccumulator;
+ brightnessAccumulator = 0;
+ }
+
+ const currentBrightness = DisplayService.getDeviceBrightness(deviceName);
+ const newBrightness = delta > 0 ? Math.min(100, currentBrightness + step) : Math.max(1, currentBrightness - step);
+ DisplayService.setBrightness(newBrightness, deviceName);
+ }
+
+ function getBrightness() {
+ const deviceName = getPinnedBrightnessDevice();
+ if (!deviceName) {
+ return;
+ }
+ return DisplayService.getDeviceBrightness(deviceName) / 100;
+ }
+
+ function getBatteryIconColor() {
+ if (!BatteryService.batteryAvailable)
+ return Theme.widgetIconColor;
+ if (BatteryService.isLowBattery && !BatteryService.isCharging)
+ return Theme.error;
+ if (BatteryService.isCharging || BatteryService.isPluggedIn)
+ return Theme.primary;
+ return Theme.widgetIconColor;
+ }
+
+ function hasPrintJobs() {
+ return CupsService.getTotalJobsNum() > 0;
+ }
+
+ function getControlCenterIconSize() {
+ return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale);
+ }
+
+ function getEffectiveControlCenterGroupOrder() {
+ const knownIds = root.defaultControlCenterGroupOrder;
+ const savedOrder = root.widgetData?.controlCenterGroupOrder;
+ const result = [];
+ const seen = {};
+
+ if (savedOrder && typeof savedOrder.length === "number") {
+ for (let i = 0; i < savedOrder.length; ++i) {
+ const groupId = savedOrder[i];
+ if (knownIds.indexOf(groupId) === -1 || seen[groupId])
+ continue;
+
+ seen[groupId] = true;
+ result.push(groupId);
+ }
+ }
+
+ for (let i = 0; i < knownIds.length; ++i) {
+ const groupId = knownIds[i];
+ if (seen[groupId])
+ continue;
+
+ seen[groupId] = true;
+ result.push(groupId);
+ }
+
+ return result;
+ }
+
+ function isGroupVisible(groupId) {
+ switch (groupId) {
+ case "screenSharing":
+ return root.showScreenSharingIcon && NiriService.hasCasts;
+ case "network":
+ return root.showNetworkIcon && NetworkService.networkAvailable;
+ case "vpn":
+ return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected;
+ case "bluetooth":
+ return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled;
+ case "audio":
+ return root.showAudioIcon;
+ case "microphone":
+ return root.showMicIcon;
+ case "brightness":
+ return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice();
+ case "battery":
+ return root.showBatteryIcon && BatteryService.batteryAvailable;
+ case "printer":
+ return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs();
+ default:
+ return false;
+ }
+ }
+
+ function isCompositeGroup(groupId) {
+ return groupId === "audio" || groupId === "microphone" || groupId === "brightness";
+ }
+
+ function getControlCenterRenderModel() {
+ return root.effectiveControlCenterGroupOrder.map(groupId => ({
+ "id": groupId,
+ "visible": root.isGroupVisible(groupId),
+ "composite": root.isCompositeGroup(groupId)
+ }));
+ }
+
+ function clearInteractionRefs() {
+ root._hAudio = null;
+ root._hBrightness = null;
+ root._hMic = null;
+ root._vAudio = null;
+ root._vBrightness = null;
+ root._vMic = null;
+ }
+
+ function registerInteractionDelegate(isVertical, item) {
+ if (!item)
+ return;
+
+ for (let i = 0; i < root._interactionDelegates.length; ++i) {
+ const entry = root._interactionDelegates[i];
+ if (entry && entry.item === item) {
+ entry.isVertical = isVertical;
+ return;
+ }
+ }
+
+ root._interactionDelegates = root._interactionDelegates.concat([
+ {
+ "isVertical": isVertical,
+ "item": item
+ }
+ ]);
+ }
+
+ function unregisterInteractionDelegate(item) {
+ if (!item)
+ return;
+
+ root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item);
+ }
+
+ function refreshInteractionRefs() {
+ root.clearInteractionRefs();
+
+ for (let i = 0; i < root._interactionDelegates.length; ++i) {
+ const entry = root._interactionDelegates[i];
+ const item = entry?.item;
+ if (!item || !item.visible)
+ continue;
+
+ const groupId = item.interactionGroupId;
+ if (entry.isVertical) {
+ if (groupId === "audio")
+ root._vAudio = item;
+ else if (groupId === "microphone")
+ root._vMic = item;
+ else if (groupId === "brightness")
+ root._vBrightness = item;
+ } else {
+ if (groupId === "audio")
+ root._hAudio = item;
+ else if (groupId === "microphone")
+ root._hMic = item;
+ else if (groupId === "brightness")
+ root._hBrightness = item;
+ }
+ }
+ }
+
+ function hasNoVisibleIcons() {
+ return !root.controlCenterRenderModel.some(entry => entry.visible);
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : controlIndicators.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? controlColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
+
+ Component.onCompleted: {
+ root._hRow = controlIndicators;
+ root._vCol = controlColumn;
+ root.clearInteractionRefs();
+ }
+
+ Column {
+ id: controlColumn
+ visible: root.isVerticalOrientation
+ width: parent.width
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: root.controlCenterRenderModel
+ Item {
+ id: verticalGroupItem
+ required property var modelData
+ required property int index
+ property string interactionGroupId: modelData.id
+
+ width: parent.width
+ height: {
+ switch (modelData.id) {
+ case "audio":
+ return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0);
+ case "microphone":
+ return root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0);
+ case "brightness":
+ return root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0);
+ default:
+ return root.vIconSize;
+ }
+ }
+ visible: modelData.visible
+
+ Component.onCompleted: {
+ root.registerInteractionDelegate(true, verticalGroupItem);
+ root.refreshInteractionRefs();
+ }
+ Component.onDestruction: {
+ if (root) {
+ root.unregisterInteractionDelegate(verticalGroupItem);
+ root.refreshInteractionRefs();
+ }
+ }
+ onVisibleChanged: root.refreshInteractionRefs()
+ onInteractionGroupIdChanged: {
+ root.refreshInteractionRefs();
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ visible: !verticalGroupItem.modelData.composite
+ name: {
+ switch (verticalGroupItem.modelData.id) {
+ case "screenSharing":
+ return "screen_record";
+ case "network":
+ return root.getNetworkIconName();
+ case "vpn":
+ return "vpn_lock";
+ case "bluetooth":
+ return "bluetooth";
+ case "battery":
+ return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
+ case "printer":
+ return "print";
+ default:
+ return "settings";
+ }
+ }
+ size: root.vIconSize
+ color: {
+ switch (verticalGroupItem.modelData.id) {
+ case "screenSharing":
+ return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
+ case "network":
+ return root.getNetworkIconColor();
+ case "vpn":
+ return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
+ case "bluetooth":
+ return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
+ case "battery":
+ return root.getBatteryIconColor();
+ case "printer":
+ return Theme.primary;
+ default:
+ return Theme.widgetIconColor;
+ }
+ }
+ }
+
+ DankIcon {
+ id: audioIconV
+ visible: verticalGroupItem.modelData.id === "audio"
+ name: root.getVolumeIconName()
+ size: root.vIconSize
+ color: Theme.widgetIconColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ }
+
+ NumericText {
+ id: audioPercentV
+ visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
+ text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
+ reserveText: "100%"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: audioIconV.bottom
+ anchors.topMargin: 2
+ }
+
+ DankIcon {
+ id: micIconV
+ visible: verticalGroupItem.modelData.id === "microphone"
+ name: root.getMicIconName()
+ size: root.vIconSize
+ color: root.getMicIconColor()
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ }
+
+ NumericText {
+ id: micPercentV
+ visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
+ text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
+ reserveText: "100%"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: micIconV.bottom
+ anchors.topMargin: 2
+ }
+
+ DankIcon {
+ id: brightnessIconV
+ visible: verticalGroupItem.modelData.id === "brightness"
+ name: root.getBrightnessIconName()
+ size: root.vIconSize
+ color: Theme.widgetIconColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ }
+
+ NumericText {
+ id: brightnessPercentV
+ visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness())
+ text: Math.round(getBrightness() * 100) + "%"
+ reserveText: "100%"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: brightnessIconV.bottom
+ anchors.topMargin: 2
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: root.vIconSize
+ visible: root.hasNoVisibleIcons()
+
+ DankIcon {
+ name: "settings"
+ size: root.vIconSize
+ color: root.isActive ? Theme.primary : Theme.widgetIconColor
+ anchors.centerIn: parent
+ }
+ }
+ }
+
+ Row {
+ id: controlIndicators
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: root.controlCenterRenderModel
+
+ Item {
+ id: horizontalGroupItem
+ required property var modelData
+ required property int index
+ property string interactionGroupId: modelData.id
+
+ width: {
+ switch (modelData.id) {
+ case "audio":
+ return audioGroup.width;
+ case "microphone":
+ return micGroup.width;
+ case "brightness":
+ return brightnessGroup.width;
+ default:
+ return root.getControlCenterIconSize();
+ }
+ }
+ implicitWidth: width
+ height: root.widgetThickness - root.horizontalPadding * 2
+ visible: modelData.visible
+
+ Component.onCompleted: {
+ root.registerInteractionDelegate(false, horizontalGroupItem);
+ root.refreshInteractionRefs();
+ }
+ Component.onDestruction: {
+ if (root) {
+ root.unregisterInteractionDelegate(horizontalGroupItem);
+ root.refreshInteractionRefs();
+ }
+ }
+ onVisibleChanged: root.refreshInteractionRefs()
+ onInteractionGroupIdChanged: {
+ root.refreshInteractionRefs();
+ }
+
+ DankIcon {
+ id: iconOnlyItem
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ visible: !horizontalGroupItem.modelData.composite
+ name: {
+ switch (horizontalGroupItem.modelData.id) {
+ case "screenSharing":
+ return "screen_record";
+ case "network":
+ return root.getNetworkIconName();
+ case "vpn":
+ return "vpn_lock";
+ case "bluetooth":
+ return "bluetooth";
+ case "battery":
+ return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
+ case "printer":
+ return "print";
+ default:
+ return "settings";
+ }
+ }
+ size: root.getControlCenterIconSize()
+ color: {
+ switch (horizontalGroupItem.modelData.id) {
+ case "screenSharing":
+ return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
+ case "network":
+ return root.getNetworkIconColor();
+ case "vpn":
+ return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
+ case "bluetooth":
+ return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
+ case "battery":
+ return root.getBatteryIconColor();
+ case "printer":
+ return Theme.primary;
+ default:
+ return Theme.widgetIconColor;
+ }
+ }
+ }
+
+ Rectangle {
+ id: audioGroup
+ width: audioContent.implicitWidth + 2
+ implicitWidth: width
+ height: parent.height
+ color: "transparent"
+ anchors.verticalCenter: parent.verticalCenter
+ visible: horizontalGroupItem.modelData.id === "audio"
+
+ Row {
+ id: audioContent
+ anchors.left: parent.left
+ anchors.leftMargin: 1
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ DankIcon {
+ id: audioIcon
+ name: root.getVolumeIconName()
+ size: root.getControlCenterIconSize()
+ color: Theme.widgetIconColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ NumericText {
+ id: audioPercent
+ visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
+ text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
+ reserveText: "100%"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ width: visible ? implicitWidth : 0
+ }
+ }
+ }
+
+ Rectangle {
+ id: micGroup
+ width: micContent.implicitWidth + 2
+ implicitWidth: width
+ height: parent.height
+ color: "transparent"
+ anchors.verticalCenter: parent.verticalCenter
+ visible: horizontalGroupItem.modelData.id === "microphone"
+
+ Row {
+ id: micContent
+ anchors.left: parent.left
+ anchors.leftMargin: 1
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ DankIcon {
+ id: micIcon
+ name: root.getMicIconName()
+ size: root.getControlCenterIconSize()
+ color: root.getMicIconColor()
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ NumericText {
+ id: micPercent
+ visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
+ text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
+ reserveText: "100%"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ width: visible ? implicitWidth : 0
+ }
+ }
+ }
+
+ Rectangle {
+ id: brightnessGroup
+ width: brightnessContent.implicitWidth + 2
+ implicitWidth: width
+ height: parent.height
+ color: "transparent"
+ anchors.verticalCenter: parent.verticalCenter
+ visible: horizontalGroupItem.modelData.id === "brightness"
+
+ Row {
+ id: brightnessContent
+ anchors.left: parent.left
+ anchors.leftMargin: 1
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ DankIcon {
+ id: brightnessIcon
+ name: root.getBrightnessIconName()
+ size: root.getControlCenterIconSize()
+ color: Theme.widgetIconColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ NumericText {
+ id: brightnessPercent
+ visible: root.showBrightnessPercent && isFinite(getBrightness())
+ text: Math.round(getBrightness() * 100) + "%"
+ reserveText: "100%"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ width: visible ? implicitWidth : 0
+ }
+ }
+ }
+ }
+ }
+
+ DankIcon {
+ name: "settings"
+ size: root.getControlCenterIconSize()
+ color: root.isActive ? Theme.primary : Theme.widgetIconColor
+ anchors.verticalCenter: parent.verticalCenter
+ visible: root.hasNoVisibleIcons()
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.NoButton
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuMonitor.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuMonitor.qml
new file mode 100644
index 0000000..212f9b5
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuMonitor.qml
@@ -0,0 +1,146 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool showPercentage: true
+ property bool showIcon: true
+ property var toggleProcessList
+ property var popoutTarget: null
+ property var widgetData: null
+ property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
+
+ signal cpuClicked
+
+ Component.onCompleted: {
+ DgopService.addRef(["cpu"]);
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["cpu"]);
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : cpuContent.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? cpuColumn.implicitHeight : cpuContent.implicitHeight
+
+ Column {
+ id: cpuColumn
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: "memory"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (DgopService.cpuUsage > 80) {
+ return Theme.tempDanger;
+ }
+
+ if (DgopService.cpuUsage > 60) {
+ return Theme.tempWarning;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null || DgopService.cpuUsage === 0) {
+ return "--";
+ }
+
+ return DgopService.cpuUsage.toFixed(0);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: cpuContent
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ id: cpuIcon
+ name: "memory"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (DgopService.cpuUsage > 80) {
+ return Theme.tempDanger;
+ }
+
+ if (DgopService.cpuUsage > 60) {
+ return Theme.tempWarning;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ id: textBox
+ anchors.verticalCenter: parent.verticalCenter
+
+ implicitWidth: root.minimumWidth ? Math.max(cpuBaseline.width, cpuCurrent.width) : cpuCurrent.width
+ implicitHeight: cpuText.implicitHeight
+
+ width: implicitWidth
+ height: implicitHeight
+
+ StyledTextMetrics {
+ id: cpuBaseline
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ text: "100%"
+ }
+
+ StyledTextMetrics {
+ id: cpuCurrent
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ text: cpuText.text
+ }
+
+ StyledText {
+ id: cpuText
+ text: {
+ const v = DgopService.cpuUsage;
+ if (v === undefined || v === null || v === 0) {
+ return "--%";
+ }
+ return v.toFixed(0) + "%";
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+
+ anchors.fill: parent
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideNone
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ DgopService.setSortBy("cpu");
+ cpuClicked();
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuTemperature.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuTemperature.qml
new file mode 100644
index 0000000..4066387
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuTemperature.qml
@@ -0,0 +1,140 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool showPercentage: true
+ property bool showIcon: true
+ property var toggleProcessList
+ property var popoutTarget: null
+ property var widgetData: null
+ property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
+
+ signal cpuTempClicked
+
+ Component.onCompleted: {
+ DgopService.addRef(["cpu"]);
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["cpu"]);
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : cpuTempRow.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? cpuTempColumn.implicitHeight : cpuTempRow.implicitHeight
+
+ Column {
+ id: cpuTempColumn
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: "device_thermostat"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (DgopService.cpuTemperature > 85) {
+ return Theme.tempDanger;
+ }
+
+ if (DgopService.cpuTemperature > 69) {
+ return Theme.tempWarning;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) {
+ return "--";
+ }
+
+ return Math.round(DgopService.cpuTemperature).toString();
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: cpuTempRow
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ id: cpuTempIcon
+ name: "device_thermostat"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (DgopService.cpuTemperature > 85) {
+ return Theme.tempDanger;
+ }
+
+ if (DgopService.cpuTemperature > 69) {
+ return Theme.tempWarning;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ id: textBox
+ anchors.verticalCenter: parent.verticalCenter
+
+ implicitWidth: root.minimumWidth ? Math.max(tempBaseline.width, cpuTempText.paintedWidth) : cpuTempText.paintedWidth
+ implicitHeight: cpuTempText.implicitHeight
+
+ width: implicitWidth
+ height: implicitHeight
+
+ StyledTextMetrics {
+ id: tempBaseline
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ text: "88°"
+ }
+
+ StyledText {
+ id: cpuTempText
+ text: {
+ if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) {
+ return "--°";
+ }
+
+ return Math.round(DgopService.cpuTemperature) + "°";
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+
+ anchors.fill: parent
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideNone
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ DgopService.setSortBy("cpu");
+ cpuTempClicked();
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DWLLayout.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DWLLayout.qml
new file mode 100644
index 0000000..f810cb6
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DWLLayout.qml
@@ -0,0 +1,110 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: layout
+
+ property bool layoutPopupVisible: false
+ property var popoutTarget: null
+
+ signal toggleLayoutPopup()
+
+ visible: CompositorService.isDwl && DwlService.dwlAvailable
+
+ property var outputState: parentScreen ? DwlService.getOutputState(parentScreen.name) : null
+ property string currentLayoutSymbol: outputState?.layoutSymbol || ""
+ property int currentLayoutIndex: outputState?.layout || 0
+
+ readonly property var layoutIcons: ({
+ "CT": "view_compact",
+ "G": "grid_view",
+ "K": "layers",
+ "M": "fullscreen",
+ "RT": "view_sidebar",
+ "S": "view_carousel",
+ "T": "view_quilt",
+ "VG": "grid_on",
+ "VK": "view_day",
+ "VS": "scrollable_header",
+ "VT": "clarify"
+ })
+
+ function getLayoutIcon(symbol) {
+ return layoutIcons[symbol] || "view_quilt"
+ }
+
+ Connections {
+ target: DwlService
+ function onStateChanged() {
+ outputState = parentScreen ? DwlService.getOutputState(parentScreen.name) : null
+ }
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: layout.isVerticalOrientation ? (layout.widgetThickness - layout.horizontalPadding * 2) : layoutContent.implicitWidth
+ implicitHeight: layout.isVerticalOrientation ? layoutColumn.implicitHeight : (layout.widgetThickness - layout.horizontalPadding * 2)
+
+ Column {
+ id: layoutColumn
+ visible: layout.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: layout.getLayoutIcon(layout.currentLayoutSymbol)
+ size: Theme.barIconSize(layout.barThickness, undefined, layout.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: layout.currentLayoutSymbol
+ font.pixelSize: Theme.barTextSize(layout.barThickness, layout.barConfig?.fontScale, layout.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: layoutContent
+ visible: !layout.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: (barConfig?.noBackground ?? false) ? 1 : 2
+
+ DankIcon {
+ name: layout.getLayoutIcon(layout.currentLayoutSymbol)
+ size: Theme.barIconSize(layout.barThickness, -4, layout.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: layout.currentLayoutSymbol
+ font.pixelSize: Theme.barTextSize(layout.barThickness, layout.barConfig?.fontScale, layout.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+
+ onClicked: {
+ toggleLayoutPopup()
+ }
+
+ onRightClicked: {
+ if (!parentScreen || !DwlService.dwlAvailable || DwlService.layouts.length === 0) {
+ return
+ }
+
+ const currentIndex = layout.currentLayoutIndex
+ const nextIndex = (currentIndex + 1) % DwlService.layouts.length
+
+ DwlService.setLayout(parentScreen.name, nextIndex)
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DiskUsage.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DiskUsage.qml
new file mode 100644
index 0000000..d6e6d1b
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DiskUsage.qml
@@ -0,0 +1,255 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property var widgetData: null
+ property string mountPath: (widgetData && widgetData.mountPath !== undefined) ? widgetData.mountPath : "/"
+ property int diskUsageMode: (widgetData && widgetData.diskUsageMode !== undefined) ? widgetData.diskUsageMode : 0
+ property bool isHovered: mouseArea.containsMouse
+ property bool isAutoHideBar: false
+
+ property var selectedMount: {
+ if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
+ return null;
+ }
+
+ const currentMountPath = root.mountPath || "/";
+
+ for (let i = 0; i < DgopService.diskMounts.length; i++) {
+ if (DgopService.diskMounts[i].mount === currentMountPath) {
+ return DgopService.diskMounts[i];
+ }
+ }
+
+ for (let i = 0; i < DgopService.diskMounts.length; i++) {
+ if (DgopService.diskMounts[i].mount === "/") {
+ return DgopService.diskMounts[i];
+ }
+ }
+
+ return DgopService.diskMounts[0] || null;
+ }
+
+ property real diskUsagePercent: {
+ if (!selectedMount || !selectedMount.percent) {
+ return 0;
+ }
+ const percentStr = selectedMount.percent.replace("%", "");
+ return parseFloat(percentStr) || 0;
+ }
+
+ Component.onCompleted: {
+ DgopService.addRef(["diskmounts"]);
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["diskmounts"]);
+ }
+
+ readonly property real minTooltipY: {
+ if (!parentScreen || !isVerticalOrientation) {
+ return 0;
+ }
+
+ if (isAutoHideBar) {
+ return 0;
+ }
+
+ if (parentScreen.y > 0) {
+ const spacing = barConfig?.spacing ?? 4;
+ const offset = barThickness + spacing;
+ return offset;
+ }
+
+ return 0;
+ }
+
+ Connections {
+ function onWidgetDataChanged() {
+ root.mountPath = Qt.binding(() => {
+ return (root.widgetData && root.widgetData.mountPath !== undefined) ? root.widgetData.mountPath : "/";
+ });
+
+ root.selectedMount = Qt.binding(() => {
+ if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
+ return null;
+ }
+
+ const currentMountPath = root.mountPath || "/";
+
+ for (let i = 0; i < DgopService.diskMounts.length; i++) {
+ if (DgopService.diskMounts[i].mount === currentMountPath) {
+ return DgopService.diskMounts[i];
+ }
+ }
+
+ for (let i = 0; i < DgopService.diskMounts.length; i++) {
+ if (DgopService.diskMounts[i].mount === "/") {
+ return DgopService.diskMounts[i];
+ }
+ }
+
+ return DgopService.diskMounts[0] || null;
+ });
+ }
+
+ target: SettingsData
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : diskContent.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? diskColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
+
+ Column {
+ id: diskColumn
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: "storage"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (root.diskUsagePercent > 90) {
+ return Theme.tempDanger;
+ }
+ if (root.diskUsagePercent > 75) {
+ return Theme.tempWarning;
+ }
+ return Theme.surfaceText;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
+ return "--";
+ }
+ if (!root.selectedMount) return "--";
+ switch (root.diskUsageMode) {
+ case 1: return root.selectedMount.size || "--";
+ case 2: return root.selectedMount.avail || "--";
+ case 3: return (root.selectedMount.avail || "--") + " / " + (root.selectedMount.size || "--");
+ default: return root.diskUsagePercent.toFixed(0);
+ }
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: diskContent
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 3
+
+ DankIcon {
+ name: "storage"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (root.diskUsagePercent > 90) {
+ return Theme.tempDanger;
+ }
+ if (root.diskUsagePercent > 75) {
+ return Theme.tempWarning;
+ }
+ return Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: {
+ if (!root.selectedMount) {
+ return "--";
+ }
+ return root.selectedMount.mount;
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ horizontalAlignment: Text.AlignLeft
+ elide: Text.ElideNone
+ }
+
+ StyledText {
+ text: {
+ if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
+ return "--%";
+ }
+ if (!root.selectedMount) return "--%";
+ switch (root.diskUsageMode) {
+ case 1: return root.selectedMount.size || "--";
+ case 2: return root.selectedMount.avail || "--";
+ case 3: return (root.selectedMount.avail || "--") + " / " + (root.selectedMount.size || "--");
+ default: return root.diskUsagePercent.toFixed(0) + "%";
+ }
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ horizontalAlignment: Text.AlignLeft
+ elide: Text.ElideNone
+
+ StyledTextMetrics {
+ id: diskBaseline
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ text: {
+ switch (root.diskUsageMode) {
+ case 3: return "888.8G / 888.8G";
+ case 1:
+ case 2: return "888.8G";
+ default: return "100%";
+ }
+ }
+ }
+
+ width: Math.max(diskBaseline.width, paintedWidth)
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: tooltipLoader
+ active: false
+ sourceComponent: DankTooltip {}
+ }
+
+ MouseArea {
+ id: mouseArea
+ z: 1
+ anchors.fill: parent
+ hoverEnabled: root.isVerticalOrientation
+ onEntered: {
+ if (root.isVerticalOrientation && root.selectedMount) {
+ tooltipLoader.active = true;
+ if (tooltipLoader.item) {
+ const globalPos = mapToGlobal(width / 2, height / 2);
+ const currentScreen = root.parentScreen || Screen;
+ const screenX = currentScreen ? currentScreen.x : 0;
+ const screenY = currentScreen ? currentScreen.y : 0;
+ const relativeY = globalPos.y - screenY;
+ const adjustedY = relativeY + root.minTooltipY;
+ const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (currentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
+ const isLeft = root.axis?.edge === "left";
+ tooltipLoader.item.show(root.selectedMount.mount, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft);
+ }
+ }
+ }
+ onExited: {
+ if (tooltipLoader.item) {
+ tooltipLoader.item.hide();
+ }
+ tooltipLoader.active = false;
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/FocusedApp.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/FocusedApp.qml
new file mode 100644
index 0000000..a4ad4d2
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/FocusedApp.qml
@@ -0,0 +1,317 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Widgets
+import Quickshell.Hyprland
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property var widgetData: null
+ property bool compactMode: widgetData?.focusedWindowCompactMode !== undefined ? widgetData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode
+ property int availableWidth: 400
+ readonly property int maxNormalWidth: 456
+ readonly property int maxCompactWidth: 288
+ property Toplevel activeWindow: null
+ property var activeDesktopEntry: null
+ property bool isHovered: mouseArea.containsMouse
+ property bool isAutoHideBar: false
+
+ readonly property real minTooltipY: {
+ if (!parentScreen || !isVerticalOrientation) {
+ return 0;
+ }
+
+ if (isAutoHideBar) {
+ return 0;
+ }
+
+ if (parentScreen.y > 0) {
+ return barThickness + (barSpacing || 4);
+ }
+
+ return 0;
+ }
+
+ function updateActiveWindow() {
+ const active = ToplevelManager.activeToplevel;
+
+ if (!active) {
+ // Only clear if our tracked window is no longer alive
+ if (activeWindow) {
+ const alive = ToplevelManager.toplevels?.values;
+ if (alive && !Array.from(alive).some(t => t === activeWindow))
+ activeWindow = null;
+ }
+ return;
+ }
+
+ if (!parentScreen || CompositorService.filterCurrentDisplay([active], parentScreen?.name)?.length > 0) {
+ activeWindow = active;
+ }
+ // else: active window is on a different screen so keep the previous value
+ }
+
+ Component.onCompleted: {
+ updateActiveWindow();
+ updateDesktopEntry();
+ }
+
+ Connections {
+ target: ToplevelManager
+ function onActiveToplevelChanged() {
+ root.updateActiveWindow();
+ }
+ }
+
+ Connections {
+ target: CompositorService
+ function onToplevelsChanged() {
+ root.updateActiveWindow();
+ }
+ }
+
+ Connections {
+ target: DesktopEntries
+ function onApplicationsChanged() {
+ root.updateDesktopEntry();
+ }
+ }
+
+ Connections {
+ target: root
+ function onActiveWindowChanged() {
+ root.updateDesktopEntry();
+ }
+ }
+
+ Connections {
+ target: SettingsData
+ function onAppIdSubstitutionsChanged() {
+ root.updateDesktopEntry();
+ }
+ }
+
+ function updateDesktopEntry() {
+ if (activeWindow && activeWindow.appId) {
+ const moddedId = Paths.moddedAppId(activeWindow.appId);
+ activeDesktopEntry = DesktopEntries.heuristicLookup(moddedId);
+ } else {
+ activeDesktopEntry = null;
+ }
+ }
+ readonly property bool hasWindowsOnCurrentWorkspace: {
+ if (CompositorService.isNiri) {
+ let currentWorkspaceId = null;
+ for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
+ const ws = NiriService.allWorkspaces[i];
+ if (ws.is_focused) {
+ currentWorkspaceId = ws.id;
+ break;
+ }
+ }
+
+ if (!currentWorkspaceId) {
+ return false;
+ }
+
+ const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId);
+ return workspaceWindows.length > 0 && activeWindow && (activeWindow.title || activeWindow.appId);
+ }
+
+ if (CompositorService.isHyprland) {
+ if (!Hyprland.focusedWorkspace || !activeWindow || !(activeWindow.title || activeWindow.appId)) {
+ return false;
+ }
+
+ try {
+ if (!Hyprland.toplevels)
+ return false;
+ const hyprlandToplevels = Array.from(Hyprland.toplevels.values);
+ const activeHyprToplevel = hyprlandToplevels.find(t => t?.wayland === activeWindow);
+
+ if (!activeHyprToplevel || !activeHyprToplevel.workspace) {
+ return false;
+ }
+
+ return activeHyprToplevel.workspace.id === Hyprland.focusedWorkspace.id;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ return activeWindow && (activeWindow.title || activeWindow.appId);
+ }
+
+ width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
+ height: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? visualHeight : barThickness) : 0
+ visible: hasWindowsOnCurrentWorkspace
+
+ content: Component {
+ Item {
+ implicitWidth: {
+ if (!root.hasWindowsOnCurrentWorkspace)
+ return 0;
+ if (root.isVerticalOrientation)
+ return root.widgetThickness - root.horizontalPadding * 2;
+ const baseWidth = contentRow.implicitWidth;
+ return compactMode ? Math.min(baseWidth, maxCompactWidth - root.horizontalPadding * 2) : Math.min(baseWidth, maxNormalWidth - root.horizontalPadding * 2);
+ }
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+ clip: false
+
+ IconImage {
+ id: appIcon
+ anchors.centerIn: parent
+ width: 18
+ height: 18
+ visible: root.isVerticalOrientation && activeWindow && status === Image.Ready
+ source: {
+ if (!activeWindow || !activeWindow.appId)
+ return "";
+ return Paths.getAppIcon(activeWindow.appId, activeDesktopEntry);
+ }
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ layer.enabled: activeWindow && (activeWindow.appId === "org.quickshell" || activeWindow.appId === "com.danklinux.dms")
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.primary
+ }
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ size: 18
+ name: "sports_esports"
+ color: Theme.widgetTextColor
+ visible: root.isVerticalOrientation && activeWindow && activeWindow.appId && appIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: root.isVerticalOrientation && activeWindow && activeWindow.appId && appIcon.status !== Image.Ready && !Paths.isSteamApp(activeWindow.appId)
+ text: {
+ if (!activeWindow || !activeWindow.appId)
+ return "?";
+ const appName = Paths.getAppName(activeWindow.appId, activeDesktopEntry);
+ return appName.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+ }
+
+ Row {
+ id: contentRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+ visible: !root.isVerticalOrientation
+
+ StyledText {
+ id: appText
+ text: {
+ if (!activeWindow || !activeWindow.appId)
+ return "";
+ return Paths.getAppName(activeWindow.appId, activeDesktopEntry);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ 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.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.outlineButton
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !compactMode && appText.text && titleText.text
+ }
+
+ StyledText {
+ id: titleText
+ text: {
+ const title = activeWindow && activeWindow.title ? activeWindow.title : "";
+ const appName = appText.text;
+
+ if (compactMode) {
+ if (!title || title === appName)
+ return title || appName;
+ if (title.endsWith(appName))
+ return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "") || appName;
+ return title;
+ }
+
+ if (!title || !appName)
+ return title;
+
+ if (title.endsWith(appName))
+ return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
+
+ return title;
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ width: Math.min(implicitWidth, compactMode ? 280 : 250)
+ visible: text.length > 0
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: root.isVerticalOrientation
+ acceptedButtons: Qt.NoButton
+ onEntered: {
+ if (root.isVerticalOrientation && activeWindow && activeWindow.appId && root.parentScreen) {
+ tooltipLoader.active = true;
+ if (tooltipLoader.item) {
+ const globalPos = mapToGlobal(width / 2, height / 2);
+ const currentScreen = root.parentScreen;
+ const screenX = currentScreen ? currentScreen.x : 0;
+ const screenY = currentScreen ? currentScreen.y : 0;
+ const relativeY = globalPos.y - screenY;
+ // Add minTooltipY offset to account for top bar
+ const adjustedY = relativeY + root.minTooltipY;
+ const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS) : (currentScreen.width - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS);
+
+ const appName = Paths.getAppName(activeWindow.appId, activeDesktopEntry);
+ const title = activeWindow.title || "";
+ const tooltipText = appName + (title ? " • " + title : "");
+
+ const isLeft = root.axis?.edge === "left";
+ tooltipLoader.item.show(tooltipText, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft);
+ }
+ }
+ }
+ onExited: {
+ if (tooltipLoader.item) {
+ tooltipLoader.item.hide();
+ }
+ tooltipLoader.active = false;
+ }
+ }
+
+ Loader {
+ id: tooltipLoader
+ active: false
+ sourceComponent: DankTooltip {}
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/GpuTemperature.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/GpuTemperature.qml
new file mode 100644
index 0000000..cf07f3e
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/GpuTemperature.qml
@@ -0,0 +1,224 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool showPercentage: true
+ property bool showIcon: true
+ property var toggleProcessList
+ property var popoutTarget: null
+ property var widgetData: null
+ property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex !== undefined) ? widgetData.selectedGpuIndex : 0
+ property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
+
+ signal gpuTempClicked
+
+ property real displayTemp: {
+ if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
+ return 0;
+ }
+
+ if (selectedGpuIndex >= 0 && selectedGpuIndex < DgopService.availableGpus.length) {
+ return DgopService.availableGpus[selectedGpuIndex].temperature || 0;
+ }
+
+ return 0;
+ }
+
+ function updateWidgetPciId(pciId) {
+ const sections = ["left", "center", "right"];
+ const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
+ if (!defaultBar)
+ return;
+ for (let s = 0; s < sections.length; s++) {
+ const sectionId = sections[s];
+ let widgets = [];
+ if (sectionId === "left") {
+ widgets = (defaultBar.leftWidgets || []).slice();
+ } else if (sectionId === "center") {
+ widgets = (defaultBar.centerWidgets || []).slice();
+ } else if (sectionId === "right") {
+ widgets = (defaultBar.rightWidgets || []).slice();
+ }
+ for (let i = 0; i < widgets.length; i++) {
+ const widget = widgets[i];
+ if (typeof widget === "object" && widget.id === "gpuTemp" && (!widget.pciId || widget.pciId === "")) {
+ widgets[i] = {
+ "id": widget.id,
+ "enabled": widget.enabled !== undefined ? widget.enabled : true,
+ "selectedGpuIndex": 0,
+ "pciId": pciId
+ };
+ if (sectionId === "left") {
+ SettingsData.setDankBarLeftWidgets(widgets);
+ } else if (sectionId === "center") {
+ SettingsData.setDankBarCenterWidgets(widgets);
+ } else if (sectionId === "right") {
+ SettingsData.setDankBarRightWidgets(widgets);
+ }
+ return;
+ }
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ DgopService.addRef(["gpu"]);
+ if (widgetData && widgetData.pciId) {
+ DgopService.addGpuPciId(widgetData.pciId);
+ } else {
+ autoSaveTimer.running = true;
+ }
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["gpu"]);
+ if (widgetData && widgetData.pciId) {
+ DgopService.removeGpuPciId(widgetData.pciId);
+ }
+ }
+
+ Connections {
+ function onWidgetDataChanged() {
+ root.selectedGpuIndex = Qt.binding(() => {
+ return (root.widgetData && root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0;
+ });
+ }
+
+ target: SettingsData
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : gpuTempRow.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? gpuTempColumn.implicitHeight : gpuTempRow.implicitHeight
+
+ Column {
+ id: gpuTempColumn
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: "auto_awesome_mosaic"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (root.displayTemp > 80) {
+ return Theme.tempDanger;
+ }
+
+ if (root.displayTemp > 65) {
+ return Theme.tempWarning;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) {
+ return "--";
+ }
+
+ return Math.round(root.displayTemp).toString();
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: gpuTempRow
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ id: gpuTempIcon
+ name: "auto_awesome_mosaic"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (root.displayTemp > 80) {
+ return Theme.tempDanger;
+ }
+
+ if (root.displayTemp > 65) {
+ return Theme.tempWarning;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ id: textBox
+ anchors.verticalCenter: parent.verticalCenter
+
+ implicitWidth: root.minimumWidth ? Math.max(gpuTempBaseline.width, gpuTempText.paintedWidth) : gpuTempText.paintedWidth
+ implicitHeight: gpuTempText.implicitHeight
+
+ width: implicitWidth
+ height: implicitHeight
+
+ StyledTextMetrics {
+ id: gpuTempBaseline
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ text: "88°"
+ }
+
+ StyledText {
+ id: gpuTempText
+ text: {
+ if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) {
+ return "--°";
+ }
+
+ return Math.round(root.displayTemp) + "°";
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+
+ anchors.fill: parent
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideNone
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ DgopService.setSortBy("cpu");
+ gpuTempClicked();
+ }
+ }
+
+ Timer {
+ id: autoSaveTimer
+
+ interval: 100
+ running: false
+ onTriggered: {
+ if (DgopService.availableGpus && DgopService.availableGpus.length > 0) {
+ const firstGpu = DgopService.availableGpus[0];
+ if (firstGpu && firstGpu.pciId) {
+ updateWidgetPciId(firstGpu.pciId);
+ DgopService.addGpuPciId(firstGpu.pciId);
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/IdleInhibitor.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/IdleInhibitor.qml
new file mode 100644
index 0000000..a8e5993
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/IdleInhibitor.qml
@@ -0,0 +1,36 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ content: Component {
+ Item {
+ implicitWidth: icon.width
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ id: icon
+ anchors.centerIn: parent
+ name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetTextColor
+ }
+ }
+ }
+
+ MouseArea {
+ z: 1
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ }
+ onClicked: {
+ SessionService.toggleIdleInhibit();
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml
new file mode 100644
index 0000000..8b49a7d
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml
@@ -0,0 +1,268 @@
+import QtQuick
+import Quickshell
+import Quickshell.Hyprland
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property var widgetData: null
+ property bool compactMode: widgetData?.keyboardLayoutNameCompactMode !== undefined ? widgetData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode
+ readonly property var langCodes: ({
+ "afrikaans": "af",
+ "albanian": "sq",
+ "amharic": "am",
+ "arabic": "ar",
+ "armenian": "hy",
+ "azerbaijani": "az",
+ "basque": "eu",
+ "belarusian": "be",
+ "bengali": "bn",
+ "bosnian": "bs",
+ "bulgarian": "bg",
+ "burmese": "my",
+ "catalan": "ca",
+ "chinese": "zh",
+ "croatian": "hr",
+ "czech": "cs",
+ "danish": "da",
+ "dutch": "nl",
+ "english": "en",
+ "esperanto": "eo",
+ "estonian": "et",
+ "filipino": "fil",
+ "finnish": "fi",
+ "french": "fr",
+ "galician": "gl",
+ "georgian": "ka",
+ "german": "de",
+ "greek": "el",
+ "gujarati": "gu",
+ "hausa": "ha",
+ "hebrew": "he",
+ "hindi": "hi",
+ "hungarian": "hu",
+ "icelandic": "is",
+ "igbo": "ig",
+ "indonesian": "id",
+ "irish": "ga",
+ "italian": "it",
+ "japanese": "ja",
+ "javanese": "jv",
+ "kannada": "kn",
+ "kazakh": "kk",
+ "khmer": "km",
+ "korean": "ko",
+ "kurdish": "ku",
+ "kyrgyz": "ky",
+ "lao": "lo",
+ "latvian": "lv",
+ "lithuanian": "lt",
+ "luxembourgish": "lb",
+ "macedonian": "mk",
+ "malay": "ms",
+ "malayalam": "ml",
+ "maltese": "mt",
+ "maori": "mi",
+ "marathi": "mr",
+ "mongolian": "mn",
+ "nepali": "ne",
+ "norwegian": "no",
+ "pashto": "ps",
+ "persian": "fa",
+ "iranian": "fa",
+ "farsi": "fa",
+ "polish": "pl",
+ "portuguese": "pt",
+ "punjabi": "pa",
+ "romanian": "ro",
+ "russian": "ru",
+ "serbian": "sr",
+ "sindhi": "sd",
+ "sinhala": "si",
+ "slovak": "sk",
+ "slovenian": "sl",
+ "somali": "so",
+ "spanish": "es",
+ "swahili": "sw",
+ "swedish": "sv",
+ "tajik": "tg",
+ "tamil": "ta",
+ "tatar": "tt",
+ "telugu": "te",
+ "thai": "th",
+ "tibetan": "bo",
+ "turkish": "tr",
+ "turkmen": "tk",
+ "ukrainian": "uk",
+ "urdu": "ur",
+ "uyghur": "ug",
+ "uzbek": "uz",
+ "vietnamese": "vi",
+ "welsh": "cy",
+ "yiddish": "yi",
+ "yoruba": "yo",
+ "zulu": "zu"
+ })
+ readonly property var validVariants: ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"]
+ property string currentLayout: {
+ if (CompositorService.isNiri) {
+ return NiriService.getCurrentKeyboardLayoutName();
+ } else if (CompositorService.isDwl) {
+ return DwlService.currentKeyboardLayout;
+ }
+ return "";
+ }
+ property string hyprlandKeyboard: ""
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
+
+ Column {
+ id: contentColumn
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: "keyboard"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ if (!root.currentLayout)
+ return "";
+ const lang = root.currentLayout.split(" ")[0].toLowerCase();
+ const code = root.langCodes[lang] || lang.substring(0, 2);
+ return code.toUpperCase();
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: contentRow
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: {
+ if (!root.currentLayout)
+ return "";
+ if (root.compactMode && !CompositorService.isHyprland) {
+ const match = root.currentLayout.match(/^(\S+)(?:.*\(([^)]+)\))?/);
+ if (match) {
+ const lang = match[1].toLowerCase();
+ const code = root.langCodes[lang] || lang.substring(0, 2);
+ if (match[2]) {
+ const variant = match[2].trim();
+ const isValid = root.validVariants.some(v => variant.toUpperCase().includes(v.toUpperCase())) || variant.length <= 3;
+ if (isValid)
+ return code + "-" + variant;
+ }
+ return code.toUpperCase();
+ }
+ return root.currentLayout.substring(0, 2).toUpperCase();
+ }
+ return root.currentLayout;
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ z: 1
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ }
+ onClicked: {
+ if (CompositorService.isNiri) {
+ NiriService.cycleKeyboardLayout();
+ } else if (CompositorService.isHyprland) {
+ Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
+ } else if (CompositorService.isDwl) {
+ Quickshell.execDetached(["mmsg", "-d", "switch_keyboard_layout"]);
+ }
+ }
+ }
+
+ Connections {
+ target: CompositorService.isHyprland ? Hyprland : null
+ enabled: CompositorService.isHyprland
+
+ function onRawEvent(event) {
+ if (event.name === "activelayout") {
+ updateLayout();
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ if (CompositorService.isHyprland) {
+ updateLayout();
+ }
+ }
+
+ function updateLayout() {
+ if (CompositorService.isHyprland) {
+ Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => {
+ if (exitCode !== 0) {
+ root.currentLayout = "Unknown";
+ return;
+ }
+ try {
+ const data = JSON.parse(output);
+ const mainKeyboard = data.keyboards.find(kb => kb.main === true);
+ root.hyprlandKeyboard = mainKeyboard.name;
+
+ if (mainKeyboard) {
+ const layout = mainKeyboard.layout;
+ const variant = mainKeyboard.variant;
+ const index = mainKeyboard.active_layout_index;
+
+ if (root.compactMode && layout && index !== undefined) {
+ const layouts = mainKeyboard.layout.split(",");
+ const variants = mainKeyboard.variant.split(",");
+ const index = mainKeyboard.active_layout_index;
+
+ if (layouts[index] && variants[index] !== undefined) {
+ if (variants[index] === "") {
+ root.currentLayout = layouts[index];
+ } else {
+ root.currentLayout = layouts[index] + "-" + variants[index];
+ }
+ } else {
+ root.currentLayout = layouts[index];
+ }
+ } else if (mainKeyboard && mainKeyboard.active_keymap) {
+ root.currentLayout = mainKeyboard.active_keymap;
+ } else {
+ root.currentLayout = "Unknown";
+ }
+ } else {
+ root.currentLayout = "Unknown";
+ }
+ } catch (e) {
+ root.currentLayout = "Unknown";
+ }
+ });
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/LauncherButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/LauncherButton.qml
new file mode 100644
index 0000000..81e6457
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/LauncherButton.qml
@@ -0,0 +1,123 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell.Widgets
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool isActive: false
+ property var hyprlandOverviewLoader: null
+
+ content: Component {
+ Item {
+ implicitWidth: root.widgetThickness - root.horizontalPadding * 2
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ visible: SettingsData.launcherLogoMode === "apps"
+ anchors.centerIn: parent
+ name: "apps"
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetIconColor
+ }
+
+ SystemLogo {
+ visible: SettingsData.launcherLogoMode === "os"
+ anchors.centerIn: parent
+ width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ colorOverride: Theme.effectiveLogoColor
+ brightnessOverride: SettingsData.launcherLogoBrightness
+ contrastOverride: SettingsData.launcherLogoContrast
+ }
+
+ IconImage {
+ visible: SettingsData.launcherLogoMode === "dank"
+ anchors.centerIn: parent
+ width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ source: "file://" + Theme.shellDir + "/assets/danklogo.svg"
+ layer.enabled: Theme.effectiveLogoColor !== ""
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.effectiveLogoColor
+ }
+ }
+
+ IconImage {
+ visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
+ anchors.centerIn: parent
+ width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ smooth: true
+ asynchronous: true
+ source: {
+ if (CompositorService.isNiri) {
+ return "file://" + Theme.shellDir + "/assets/niri.svg";
+ } else if (CompositorService.isHyprland) {
+ return "file://" + Theme.shellDir + "/assets/hyprland.svg";
+ } else if (CompositorService.isDwl) {
+ return "file://" + Theme.shellDir + "/assets/mango.png";
+ } else if (CompositorService.isSway) {
+ return "file://" + Theme.shellDir + "/assets/sway.svg";
+ } else if (CompositorService.isScroll) {
+ return "file://" + Theme.shellDir + "/assets/sway.svg";
+ } else if (CompositorService.isMiracle) {
+ return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
+ } else if (CompositorService.isLabwc) {
+ return "file://" + Theme.shellDir + "/assets/labwc.png";
+ }
+ return "";
+ }
+ layer.enabled: Theme.effectiveLogoColor !== ""
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.effectiveLogoColor
+ brightness: {
+ SettingsData.launcherLogoBrightness;
+ }
+ contrast: {
+ SettingsData.launcherLogoContrast;
+ }
+ }
+ }
+
+ IconImage {
+ visible: SettingsData.launcherLogoMode === "custom" && SettingsData.launcherLogoCustomPath !== ""
+ anchors.centerIn: parent
+ width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ smooth: true
+ asynchronous: true
+ source: SettingsData.launcherLogoCustomPath ? "file://" + SettingsData.launcherLogoCustomPath.replace("file://", "") : ""
+ layer.enabled: Theme.effectiveLogoColor !== ""
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.effectiveLogoColor
+ brightness: SettingsData.launcherLogoBrightness
+ contrast: SettingsData.launcherLogoContrast
+ }
+ }
+ }
+ }
+
+ onRightClicked: {
+ if (CompositorService.isNiri) {
+ NiriService.toggleOverview();
+ } else if (root.hyprlandOverviewLoader?.item) {
+ root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Media.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Media.qml
new file mode 100644
index 0000000..6007427
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Media.qml
@@ -0,0 +1,502 @@
+import QtQuick
+import Quickshell.Services.Mpris
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ readonly property MprisPlayer activePlayer: MprisController.activePlayer
+ readonly property bool playerAvailable: activePlayer !== null
+ readonly property bool __isChromeBrowser: {
+ if (!activePlayer?.identity)
+ return false;
+ const id = activePlayer.identity.toLowerCase();
+ return id.includes("chrome") || id.includes("chromium");
+ }
+ readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
+ property bool compactMode: false
+ property var widgetData: null
+ readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled
+ readonly property int maxTextWidth: {
+ const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
+ switch (size) {
+ case 0:
+ return 0;
+ case 2:
+ return 180;
+ case 3:
+ return 240;
+ default:
+ return 120;
+ }
+ }
+ readonly property int currentContentWidth: {
+ if (isVerticalOrientation) {
+ return widgetThickness - horizontalPadding * 2;
+ }
+ return 0;
+ }
+ readonly property int currentContentHeight: {
+ if (!isVerticalOrientation) {
+ return widgetThickness - horizontalPadding * 2;
+ }
+ const audioVizHeight = 20;
+ const playButtonHeight = 24;
+ return audioVizHeight + Theme.spacingXS + playButtonHeight;
+ }
+
+ property real scrollAccumulatorY: 0
+ property real touchpadThreshold: 100
+
+ onWheel: function (wheelEvent) {
+ if (SettingsData.audioScrollMode === "nothing")
+ return;
+
+ if (SettingsData.audioScrollMode === "volume") {
+ if (!usePlayerVolume)
+ return;
+
+ wheelEvent.accepted = true;
+
+ const deltaY = wheelEvent.angleDelta.y;
+ const isMouseWheelY = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
+
+ const currentVolume = activePlayer.volume * 100;
+
+ let newVolume = currentVolume;
+ if (isMouseWheelY) {
+ if (deltaY > 0) {
+ newVolume = Math.min(100, currentVolume + SettingsData.audioWheelScrollAmount);
+ } else if (deltaY < 0) {
+ newVolume = Math.max(0, currentVolume - SettingsData.audioWheelScrollAmount);
+ }
+ } else {
+ scrollAccumulatorY += deltaY;
+ if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
+ if (scrollAccumulatorY > 0) {
+ newVolume = Math.min(100, currentVolume + 1);
+ } else {
+ newVolume = Math.max(0, currentVolume - 1);
+ }
+ scrollAccumulatorY = 0;
+ }
+ }
+
+ activePlayer.volume = newVolume / 100;
+ } else if (SettingsData.audioScrollMode === "song") {
+ if (!activePlayer)
+ return;
+
+ wheelEvent.accepted = true;
+
+ const deltaY = wheelEvent.angleDelta.y;
+ const isMouseWheelY = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
+
+ if (isMouseWheelY) {
+ if (deltaY > 0) {
+ MprisController.previousOrRewind();
+ } else {
+ activePlayer.next();
+ }
+ } else {
+ scrollAccumulatorY += deltaY;
+ if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
+ if (scrollAccumulatorY > 0) {
+ MprisController.previousOrRewind();
+ } else {
+ activePlayer.next();
+ }
+ scrollAccumulatorY = 0;
+ }
+ }
+ }
+ }
+
+ content: Component {
+ Item {
+ id: contentRoot
+ readonly property real measuredTextWidth: {
+ if (!root.playerAvailable || root.maxTextWidth <= 0 || !textContainer.visible)
+ return 0;
+ // Preserve the fixed-width text slot even if metadata is briefly empty.
+ if (!root.adaptiveWidthEnabled)
+ return root.maxTextWidth;
+ if (textContainer.displayText.length === 0)
+ return 0;
+ const rawWidth = mediaText.contentWidth;
+ if (!isFinite(rawWidth) || rawWidth <= 0)
+ return 0;
+ return Math.min(root.maxTextWidth, Math.ceil(rawWidth));
+ }
+ readonly property int horizontalContentWidth: {
+ const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
+ const audioVizWidth = 20;
+ const baseWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
+ return baseWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
+ }
+
+ implicitWidth: root.playerAvailable ? (root.isVerticalOrientation ? root.currentContentWidth : horizontalContentWidth) : 0
+ implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
+ opacity: root.playerAvailable ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on implicitWidth {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
+ }
+ }
+
+ Behavior on implicitHeight {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Column {
+ id: verticalLayout
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ Item {
+ width: 20
+ height: 20
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ AudioVisualization {
+ anchors.fill: parent
+ visible: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled
+ }
+
+ DankIcon {
+ anchors.fill: parent
+ name: "music_note"
+ size: 20
+ color: Theme.primary
+ visible: !CavaService.cavaAvailable || !SettingsData.audioVisualizerEnabled
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ }
+ onClicked: {
+ if (root.popoutTarget && root.popoutTarget.setTriggerPosition) {
+ const globalPos = parent.mapToItem(null, 0, 0);
+ const currentScreen = root.parentScreen || Screen;
+ const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, parent.width, root.barSpacing, barPosition, root.barConfig);
+ root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig);
+ }
+ root.clicked();
+ }
+ }
+ }
+
+ Rectangle {
+ width: 24
+ height: 24
+ radius: 12
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
+ visible: root.playerAvailable
+ opacity: activePlayer ? 1 : 0.3
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
+ size: 14
+ color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.playerAvailable
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
+ onClicked: mouse => {
+ if (!activePlayer)
+ return;
+ if (mouse.button === Qt.LeftButton) {
+ activePlayer.togglePlaying();
+ } else if (mouse.button === Qt.MiddleButton) {
+ MprisController.previousOrRewind();
+ } else if (mouse.button === Qt.RightButton) {
+ activePlayer.next();
+ }
+ }
+ }
+ }
+ }
+
+ Row {
+ id: mediaRow
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ Row {
+ id: mediaInfo
+ spacing: Theme.spacingXS
+
+ Item {
+ width: 20
+ height: 20
+ anchors.verticalCenter: parent.verticalCenter
+
+ AudioVisualization {
+ anchors.fill: parent
+ visible: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled
+ }
+
+ DankIcon {
+ anchors.fill: parent
+ name: "music_note"
+ size: 20
+ color: Theme.primary
+ visible: !CavaService.cavaAvailable || !SettingsData.audioVisualizerEnabled
+ }
+ }
+
+ Rectangle {
+ id: textContainer
+ readonly property string cachedIdentity: activePlayer ? (activePlayer.identity || "") : ""
+ readonly property string lowerIdentity: cachedIdentity.toLowerCase()
+ readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari")
+
+ property string displayText: {
+ if (!activePlayer || !activePlayer.trackTitle) {
+ return "";
+ }
+
+ const title = isWebMedia ? activePlayer.trackTitle : (activePlayer.trackTitle || "Unknown Track");
+ const subtitle = isWebMedia ? (activePlayer.trackArtist || cachedIdentity) : (activePlayer.trackArtist || "");
+ return subtitle.length > 0 ? title + " • " + subtitle : title;
+ }
+
+ anchors.verticalCenter: parent.verticalCenter
+ width: contentRoot.measuredTextWidth
+ height: root.widgetThickness
+ visible: {
+ const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
+ return size > 0;
+ }
+ clip: true
+ color: "transparent"
+
+ Behavior on width {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
+ }
+ }
+
+ Item {
+ id: textClip
+ anchors.fill: parent
+ clip: true
+
+ StyledText {
+ id: mediaText
+ property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
+ property real scrollOffset: 0
+ property real textShift: 0
+
+ anchors.verticalCenter: parent.verticalCenter
+ text: textContainer.displayText
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ wrapMode: Text.NoWrap
+ x: (needsScrolling ? -scrollOffset : 0) + textShift
+ opacity: 1
+
+ onTextChanged: {
+ scrollOffset = 0;
+ textShift = 0;
+ scrollAnimation.restart();
+ textChangeAnimation.restart();
+ }
+
+ SequentialAnimation {
+ id: scrollAnimation
+ running: mediaText.needsScrolling && textContainer.visible
+ loops: Animation.Infinite
+
+ PauseAnimation {
+ duration: 2000
+ }
+
+ NumberAnimation {
+ target: mediaText
+ property: "scrollOffset"
+ from: 0
+ to: mediaText.implicitWidth - textContainer.width + 5
+ duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
+ easing.type: Easing.Linear
+ }
+
+ PauseAnimation {
+ duration: 2000
+ }
+
+ NumberAnimation {
+ target: mediaText
+ property: "scrollOffset"
+ to: 0
+ duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
+ easing.type: Easing.Linear
+ }
+ }
+
+ SequentialAnimation {
+ id: textChangeAnimation
+
+ ParallelAnimation {
+ NumberAnimation {
+ target: mediaText
+ property: "opacity"
+ from: 0.7
+ to: 1
+ duration: Theme.shortDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
+ }
+
+ NumberAnimation {
+ target: mediaText
+ property: "textShift"
+ from: 4
+ to: 0
+ duration: Theme.shortDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.playerAvailable
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ if (root.popoutTarget && root.popoutTarget.setTriggerPosition) {
+ const globalPos = mapToItem(null, 0, 0);
+ const currentScreen = root.parentScreen || Screen;
+ const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, root.width, root.barSpacing, barPosition, root.barConfig);
+ root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig);
+ }
+ root.clicked();
+ }
+ }
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ width: 20
+ height: 20
+ radius: 10
+ anchors.verticalCenter: parent.verticalCenter
+ color: prevArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+ visible: root.playerAvailable
+ opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_previous"
+ size: 12
+ color: Theme.widgetTextColor
+ }
+
+ MouseArea {
+ id: prevArea
+ anchors.fill: parent
+ enabled: root.playerAvailable
+ cursorShape: Qt.PointingHandCursor
+ onClicked: MprisController.previousOrRewind()
+ }
+ }
+
+ Rectangle {
+ width: 24
+ height: 24
+ radius: 12
+ anchors.verticalCenter: parent.verticalCenter
+ color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
+ visible: root.playerAvailable
+ opacity: activePlayer ? 1 : 0.3
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
+ size: 14
+ color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.playerAvailable
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (activePlayer) {
+ activePlayer.togglePlaying();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: 20
+ height: 20
+ radius: 10
+ anchors.verticalCenter: parent.verticalCenter
+ color: nextArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+ visible: playerAvailable
+ opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_next"
+ size: 12
+ color: Theme.widgetTextColor
+ }
+
+ MouseArea {
+ id: nextArea
+ anchors.fill: parent
+ enabled: root.playerAvailable
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (activePlayer) {
+ activePlayer.next();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NetworkMonitor.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NetworkMonitor.qml
new file mode 100644
index 0000000..b7d8436
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NetworkMonitor.qml
@@ -0,0 +1,147 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Modules.Plugins
+import qs.Modules.ProcessList
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ function formatNetworkSpeed(bytesPerSec) {
+ if (bytesPerSec < 1024) {
+ return bytesPerSec.toFixed(0) + " B/s"
+ } else if (bytesPerSec < 1024 * 1024) {
+ return (bytesPerSec / 1024).toFixed(1) + " KB/s"
+ } else if (bytesPerSec < 1024 * 1024 * 1024) {
+ return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
+ } else {
+ return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
+ }
+ }
+
+ Component.onCompleted: {
+ DgopService.addRef(["network"])
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["network"])
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
+
+ Column {
+ id: contentColumn
+ anchors.centerIn: parent
+ spacing: 2
+ visible: root.isVerticalOrientation
+
+ DankIcon {
+ name: "network_check"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ const rate = DgopService.networkRxRate
+ if (rate < 1024) return rate.toFixed(0)
+ if (rate < 1024 * 1024) return (rate / 1024).toFixed(0) + "K"
+ return (rate / (1024 * 1024)).toFixed(0) + "M"
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.info
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ const rate = DgopService.networkTxRate
+ if (rate < 1024) return rate.toFixed(0)
+ if (rate < 1024 * 1024) return (rate / 1024).toFixed(0) + "K"
+ return (rate / (1024 * 1024)).toFixed(0) + "M"
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.error
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: contentRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+ visible: !root.isVerticalOrientation
+
+ DankIcon {
+ name: "network_check"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Row {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 4
+
+ StyledText {
+ text: "↓"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.info
+ }
+
+ StyledText {
+ text: DgopService.networkRxRate > 0 ? root.formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ horizontalAlignment: Text.AlignLeft
+ elide: Text.ElideNone
+ wrapMode: Text.NoWrap
+
+ StyledTextMetrics {
+ id: rxBaseline
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ text: "88.8 MB/s"
+ }
+
+ width: Math.max(rxBaseline.width, paintedWidth)
+ }
+ }
+
+ Row {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 4
+
+ StyledText {
+ text: "↑"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.error
+ }
+
+ StyledText {
+ text: DgopService.networkTxRate > 0 ? root.formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s"
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ horizontalAlignment: Text.AlignLeft
+ elide: Text.ElideNone
+ wrapMode: Text.NoWrap
+
+ StyledTextMetrics {
+ id: txBaseline
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ text: "88.8 MB/s"
+ }
+
+ width: Math.max(txBaseline.width, paintedWidth)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotepadButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotepadButton.qml
new file mode 100644
index 0000000..08f5e6b
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotepadButton.qml
@@ -0,0 +1,367 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ readonly property string focusedScreenName: (CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") : CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : "")
+
+ function resolveNotepadInstance() {
+ if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) {
+ return null;
+ }
+
+ const targetScreen = focusedScreenName;
+ if (targetScreen) {
+ for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
+ var slideout = notepadSlideoutVariants.instances[i];
+ if (slideout.modelData && slideout.modelData.name === targetScreen) {
+ return slideout;
+ }
+ }
+ }
+
+ return notepadSlideoutVariants.instances.length > 0 ? notepadSlideoutVariants.instances[0] : null;
+ }
+
+ readonly property var notepadInstance: resolveNotepadInstance()
+ readonly property bool isActive: notepadInstance?.isVisible ?? false
+ property bool isAutoHideBar: false
+
+ readonly property real minTooltipY: {
+ if (!parentScreen || !(axis?.isVertical ?? false)) {
+ return 0;
+ }
+
+ if (isAutoHideBar) {
+ return 0;
+ }
+
+ if (parentScreen.y > 0) {
+ return barThickness + barSpacing;
+ }
+
+ return 0;
+ }
+
+ readonly property var savedTabEntries: {
+ const result = [];
+ const tabs = NotepadStorageService.tabs || [];
+ for (let i = 0; i < tabs.length; i++) {
+ const tab = tabs[i];
+ if (tab && !tab.isTemporary) {
+ result.push({
+ index: i,
+ tab: tab
+ });
+ }
+ }
+ return result.slice(0, 5);
+ }
+
+ function openTabByIndex(tabIndex) {
+ if (tabIndex < 0)
+ return;
+ if (root.notepadInstance && typeof root.notepadInstance.show === "function") {
+ root.notepadInstance.show();
+ }
+ Qt.callLater(() => {
+ NotepadStorageService.switchToTab(tabIndex);
+ });
+ }
+
+ function openNewNote() {
+ if (root.notepadInstance && typeof root.notepadInstance.show === "function") {
+ root.notepadInstance.show();
+ }
+ Qt.callLater(() => {
+ NotepadStorageService.createNewTab();
+ });
+ }
+
+ function openContextMenu() {
+ const screen = root.parentScreen || Screen;
+ const screenX = screen.x || 0;
+ const screenY = screen.y || 0;
+ const isVertical = root.axis?.isVertical ?? false;
+ const edge = root.axis?.edge ?? "top";
+ const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS);
+
+ const globalPos = root.mapToGlobal(root.width / 2, root.height / 2);
+ const relativeX = globalPos.x - screenX;
+ const relativeY = globalPos.y - screenY;
+
+ let anchorX = relativeX;
+ let anchorY = relativeY;
+
+ if (isVertical) {
+ anchorX = edge === "left" ? (root.barThickness + root.barSpacing + gap) : (screen.width - (root.barThickness + root.barSpacing + gap));
+ anchorY = relativeY + root.minTooltipY;
+ } else {
+ anchorX = relativeX;
+ anchorY = edge === "bottom" ? (screen.height - (root.barThickness + root.barSpacing + gap)) : (root.barThickness + root.barSpacing + gap);
+ }
+
+ contextMenuWindow.showAt(anchorX, anchorY, isVertical, edge, screen);
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: notepadIcon.width
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ id: notepadIcon
+
+ anchors.centerIn: parent
+ name: "assignment"
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: root.isActive ? Theme.primary : Theme.surfaceText
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ }
+ onClicked: function (mouse) {
+ if (mouse.button === Qt.RightButton) {
+ openContextMenu();
+ return;
+ }
+ const inst = root.notepadInstance;
+ if (inst) {
+ inst.toggle();
+ }
+ }
+ }
+
+ PanelWindow {
+ id: contextMenuWindow
+
+ WlrLayershell.namespace: "dms:notepad-context-menu"
+
+ property bool isVertical: false
+ property string edge: "top"
+ property point anchorPos: Qt.point(0, 0)
+
+ function showAt(x, y, vertical, barEdge, targetScreen) {
+ if (targetScreen) {
+ contextMenuWindow.screen = targetScreen;
+ }
+
+ anchorPos = Qt.point(x, y);
+ isVertical = vertical ?? false;
+ edge = barEdge ?? "top";
+
+ visible = true;
+
+ if (contextMenuWindow.screen) {
+ TrayMenuManager.registerMenu(contextMenuWindow.screen.name, contextMenuWindow);
+ }
+ }
+
+ function closeMenu() {
+ visible = false;
+
+ if (contextMenuWindow.screen) {
+ TrayMenuManager.unregisterMenu(contextMenuWindow.screen.name);
+ }
+ }
+
+ screen: null
+ visible: false
+ WlrLayershell.layer: WlrLayershell.Overlay
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+ color: "transparent"
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ Component.onDestruction: {
+ if (contextMenuWindow.screen) {
+ TrayMenuManager.unregisterMenu(contextMenuWindow.screen.name);
+ }
+ }
+
+ Connections {
+ target: PopoutManager
+ function onPopoutOpening() {
+ contextMenuWindow.closeMenu();
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ z: 0
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onClicked: contextMenuWindow.closeMenu()
+ }
+
+ Rectangle {
+ id: menuContainer
+ z: 1
+
+ x: {
+ if (contextMenuWindow.isVertical) {
+ if (contextMenuWindow.edge === "left") {
+ return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x);
+ }
+ return Math.max(10, contextMenuWindow.anchorPos.x - width);
+ }
+ const left = 10;
+ const right = contextMenuWindow.width - width - 10;
+ const want = contextMenuWindow.anchorPos.x - width / 2;
+ return Math.max(left, Math.min(right, want));
+ }
+ y: {
+ if (contextMenuWindow.isVertical) {
+ const top = 10;
+ const bottom = contextMenuWindow.height - height - 10;
+ const want = contextMenuWindow.anchorPos.y - height / 2;
+ return Math.max(top, Math.min(bottom, want));
+ }
+ if (contextMenuWindow.edge === "top") {
+ return Math.min(contextMenuWindow.height - height - 10, contextMenuWindow.anchorPos.y);
+ }
+ return Math.max(10, contextMenuWindow.anchorPos.y - height);
+ }
+
+ width: Math.min(260, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2))
+ height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: 1
+
+ opacity: contextMenuWindow.visible ? 1 : 0
+ visible: opacity > 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ 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: -1
+ }
+
+ Column {
+ id: menuColumn
+ width: parent.width - Theme.spacingS * 2
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: Theme.spacingS
+ spacing: 1
+
+ Repeater {
+ model: root.savedTabEntries
+
+ Rectangle {
+ required property var modelData
+
+ width: parent.width
+ height: 30
+ radius: Theme.cornerRadius
+ color: tabArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: "description"
+ size: 16
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelData.tab?.title || I18n.tr("Saved Note")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ }
+ }
+
+ MouseArea {
+ id: tabArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ contextMenuWindow.closeMenu();
+ root.openTabByIndex(modelData.index);
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 30
+ radius: Theme.cornerRadius
+ color: newNoteArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: "add"
+ size: 16
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: I18n.tr("Open a new note")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: newNoteArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ contextMenuWindow.closeMenu();
+ root.openNewNote();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml
new file mode 100644
index 0000000..d7bcc1e
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml
@@ -0,0 +1,40 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool hasUnread: false
+ property bool isActive: false
+
+ content: Component {
+ Item {
+ implicitWidth: notifIcon.width
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ id: notifIcon
+ anchors.centerIn: parent
+ name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: SessionData.doNotDisturb ? Theme.primary : (root.isActive ? Theme.primary : Theme.widgetIconColor)
+ }
+
+ Rectangle {
+ width: 6
+ height: 6
+ radius: 3
+ color: Theme.error
+ anchors.right: notifIcon.right
+ anchors.top: notifIcon.top
+ visible: root.hasUnread
+ }
+ }
+ }
+
+ onRightClicked: {
+ SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml
new file mode 100644
index 0000000..da9caee
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml
@@ -0,0 +1,25 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool isActive: false
+
+ content: Component {
+ Item {
+ implicitWidth: icon.width
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ id: icon
+ anchors.centerIn: parent
+ name: "power_settings_new"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetIconColor
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PrivacyIndicator.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PrivacyIndicator.qml
new file mode 100644
index 0000000..a09aa55
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PrivacyIndicator.qml
@@ -0,0 +1,247 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ section: "right"
+
+ property bool showMicIcon: SettingsData.privacyShowMicIcon
+ property bool showCameraIcon: SettingsData.privacyShowCameraIcon
+ property bool showScreenSharingIcon: SettingsData.privacyShowScreenShareIcon
+
+ readonly property bool hasActivePrivacy: showMicIcon || showCameraIcon || showScreenSharingIcon || PrivacyService.anyPrivacyActive
+ readonly property int activeCount: (showMicIcon ? 1 : PrivacyService.microphoneActive) + (showCameraIcon ? 1 : PrivacyService.cameraActive) + (showScreenSharingIcon ? 1 : PrivacyService.screensharingActive)
+ readonly property real contentWidth: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
+ readonly property real contentHeight: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
+
+ opacity: hasActivePrivacy ? 1 : 0
+
+ states: [
+ State {
+ name: "hidden_horizontal"
+ when: !hasActivePrivacy && !isVerticalOrientation
+ PropertyChanges {
+ target: root
+ width: 0
+ }
+ },
+ State {
+ name: "hidden_vertical"
+ when: !hasActivePrivacy && isVerticalOrientation
+ PropertyChanges {
+ target: root
+ height: 0
+ }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ NumberAnimation {
+ properties: "width,height"
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ ]
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.hasActivePrivacy ? root.contentWidth : 0
+ implicitHeight: root.hasActivePrivacy ? root.contentHeight : 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+ visible: root.isVerticalOrientation && root.hasActivePrivacy
+
+ Item {
+ width: 18
+ height: 18
+ visible: root.showMicIcon || PrivacyService.microphoneActive
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ DankIcon {
+ name: {
+ const sourceAudio = AudioService.source?.audio;
+ const muted = !sourceAudio || sourceAudio.muted || sourceAudio.volume === 0.0;
+ if (muted)
+ return "mic_off";
+ return "mic";
+ }
+ size: Theme.iconSizeSmall
+ color: PrivacyService.microphoneActive ? Theme.error : Theme.surfaceText
+ filled: PrivacyService.microphoneActive
+ anchors.centerIn: parent
+ }
+ }
+
+ Item {
+ width: 18
+ height: 18
+ visible: root.showCameraIcon || PrivacyService.cameraActive
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ DankIcon {
+ name: "camera_video"
+ size: Theme.iconSizeSmall
+ color: PrivacyService.cameraActive ? Theme.error : Theme.surfaceText
+ filled: PrivacyService.cameraActive
+ anchors.centerIn: parent
+ }
+
+ Rectangle {
+ width: 6
+ height: 6
+ radius: 3
+ color: Theme.error
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.rightMargin: -2
+ anchors.topMargin: -1
+ visible: PrivacyService.cameraActive
+ }
+ }
+
+ Item {
+ width: 18
+ height: 18
+ visible: root.showScreenSharingIcon || PrivacyService.screensharingActive
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ DankIcon {
+ name: "screen_share"
+ size: Theme.iconSizeSmall
+ color: PrivacyService.screensharingActive ? Theme.warning : Theme.surfaceText
+ filled: PrivacyService.screensharingActive
+ anchors.centerIn: parent
+ }
+ }
+ }
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+ visible: !root.isVerticalOrientation && root.hasActivePrivacy
+
+ Item {
+ width: 18
+ height: 18
+ visible: root.showMicIcon || PrivacyService.microphoneActive
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ name: {
+ const sourceAudio = AudioService.source?.audio;
+ const muted = !sourceAudio || sourceAudio.muted || sourceAudio.volume === 0.0;
+ if (muted)
+ return "mic_off";
+ return "mic";
+ }
+ size: Theme.iconSizeSmall
+ color: PrivacyService.microphoneActive ? Theme.error : Theme.surfaceText
+ filled: PrivacyService.microphoneActive
+ anchors.centerIn: parent
+ }
+ }
+
+ Item {
+ width: 18
+ height: 18
+ visible: root.showCameraIcon || PrivacyService.cameraActive
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ name: "camera_video"
+ size: Theme.iconSizeSmall
+ color: PrivacyService.cameraActive ? Theme.error : Theme.surfaceText
+ filled: PrivacyService.cameraActive
+ anchors.centerIn: parent
+ }
+
+ Rectangle {
+ width: 6
+ height: 6
+ radius: 3
+ color: Theme.error
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.rightMargin: -2
+ anchors.topMargin: -1
+ visible: PrivacyService.cameraActive
+ }
+ }
+
+ Item {
+ width: 18
+ height: 18
+ visible: root.showScreenSharingIcon || PrivacyService.screensharingActive
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ name: "screen_share"
+ size: Theme.iconSizeSmall
+ color: PrivacyService.screensharingActive ? Theme.warning : Theme.surfaceText
+ filled: PrivacyService.screensharingActive
+ anchors.centerIn: parent
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: tooltip
+ width: tooltipText.contentWidth + Theme.spacingM * 2
+ height: tooltipText.contentHeight + Theme.spacingS * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ border.color: Theme.outlineMedium
+ border.width: 1
+ visible: false
+ opacity: root.isMouseHovered && hasActivePrivacy ? 1 : 0
+ z: 100
+ x: (parent.width - width) / 2
+ y: -height - Theme.spacingXS
+
+ StyledText {
+ id: tooltipText
+ anchors.centerIn: parent
+ text: PrivacyService.getPrivacySummary()
+ font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ }
+
+ Rectangle {
+ width: 8
+ height: 8
+ color: parent.color
+ border.color: parent.border.color
+ border.width: parent.border.width
+ rotation: 45
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.bottom
+ anchors.topMargin: -4
+ }
+
+ Behavior on opacity {
+ enabled: hasActivePrivacy && root.visible
+
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RamMonitor.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RamMonitor.qml
new file mode 100644
index 0000000..f5d6de5
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RamMonitor.qml
@@ -0,0 +1,175 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool showPercentage: true
+ property bool showIcon: true
+ property var toggleProcessList
+ property var popoutTarget: null
+ property var widgetData: null
+ property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
+ property bool showSwap: (widgetData && widgetData.showSwap !== undefined) ? widgetData.showSwap : false
+ property bool showInGb: (widgetData && widgetData.showInGb !== undefined) ? widgetData.showInGb : false
+ readonly property real swapUsage: DgopService.totalSwapKB > 0 ? (DgopService.usedSwapKB / DgopService.totalSwapKB) * 100 : 0
+
+ signal ramClicked
+
+ Component.onCompleted: {
+ DgopService.addRef(["memory"]);
+ }
+ Component.onDestruction: {
+ DgopService.removeRef(["memory"]);
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : ramContent.implicitWidth
+ implicitHeight: root.isVerticalOrientation ? ramColumn.implicitHeight : ramContent.implicitHeight
+
+ Column {
+ id: ramColumn
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: "developer_board"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (DgopService.memoryUsage > 90) {
+ return Theme.tempDanger;
+ }
+
+ if (DgopService.memoryUsage > 75) {
+ return Theme.tempWarning;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) {
+ return "--";
+ }
+
+ if (root.showInGb) {
+ return (DgopService.usedMemoryMB / 1024).toFixed(1);
+ }
+
+ return DgopService.memoryUsage.toFixed(0);
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ visible: root.showSwap && DgopService.totalSwapKB > 0
+ text: root.swapUsage.toFixed(0)
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: ramContent
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ id: ramIcon
+ name: "developer_board"
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (DgopService.memoryUsage > 90) {
+ return Theme.tempDanger;
+ }
+
+ if (DgopService.memoryUsage > 75) {
+ return Theme.tempWarning;
+ }
+
+ return Theme.widgetIconColor;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ id: textBox
+ anchors.verticalCenter: parent.verticalCenter
+
+ implicitWidth: root.minimumWidth ? Math.max(ramBaseline.width, ramText.paintedWidth) : ramText.paintedWidth
+ implicitHeight: ramText.implicitHeight
+
+ width: implicitWidth
+ height: implicitHeight
+
+ StyledTextMetrics {
+ id: ramBaseline
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ text: {
+ let baseText = root.showInGb ? "88.8 GB" : "88%";
+ if (!root.showSwap) {
+ return baseText;
+ }
+ if (root.swapUsage < 10) {
+ return baseText + " · 0%";
+ }
+ return baseText + " · 88%";
+ }
+ }
+
+ StyledText {
+ id: ramText
+ text: {
+ if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) {
+ return root.showInGb ? "-- GB" : "--%";
+ }
+
+ let ramText = "";
+ if (root.showInGb) {
+ ramText = (DgopService.usedMemoryMB / 1024).toFixed(1) + " GB";
+ } else {
+ ramText = DgopService.memoryUsage.toFixed(0) + "%";
+ }
+
+ if (root.showSwap && DgopService.totalSwapKB > 0) {
+ return ramText + " · " + root.swapUsage.toFixed(0) + "%";
+ }
+ return ramText;
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+
+ anchors.fill: parent
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideNone
+ wrapMode: Text.NoWrap
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ DgopService.setSortBy("memory");
+ ramClicked();
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RunningApps.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RunningApps.qml
new file mode 100644
index 0000000..ae58802
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RunningApps.qml
@@ -0,0 +1,902 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Widgets
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ enableBackgroundHover: false
+ enableCursor: false
+ section: "left"
+
+ property var widgetData: null
+ property var hoveredItem: null
+ property var topBar: null
+ property bool isAutoHideBar: false
+ property Item windowRoot: (Window.window ? Window.window.contentItem : null)
+
+ readonly property real effectiveBarThickness: {
+ if (barThickness > 0 && barSpacing > 0) {
+ return barThickness + barSpacing;
+ }
+ const innerPadding = barConfig?.innerPadding ?? 4;
+ const spacing = barConfig?.spacing ?? 4;
+ return Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding)) + spacing;
+ }
+
+ readonly property var barBounds: {
+ if (!parentScreen || !barConfig) {
+ return {
+ "x": 0,
+ "y": 0,
+ "width": 0,
+ "height": 0,
+ "wingSize": 0
+ };
+ }
+ const barPosition = axis.edge === "left" ? 2 : (axis.edge === "right" ? 3 : (axis.edge === "top" ? 0 : 1));
+ return SettingsData.getBarBounds(parentScreen, effectiveBarThickness, barPosition, barConfig);
+ }
+
+ readonly property real barY: barBounds.y
+
+ readonly property real minTooltipY: {
+ if (!parentScreen || !isVerticalOrientation) {
+ return 0;
+ }
+
+ if (isAutoHideBar) {
+ return 0;
+ }
+
+ if (parentScreen.y > 0) {
+ return effectiveBarThickness;
+ }
+
+ return 0;
+ }
+
+ property int _desktopEntriesUpdateTrigger: 0
+ property int _toplevelsUpdateTrigger: 0
+ property int _appIdSubstitutionsTrigger: 0
+
+ readonly property bool _currentWorkspace: widgetData?.runningAppsCurrentWorkspace !== undefined ? widgetData.runningAppsCurrentWorkspace : SettingsData.runningAppsCurrentWorkspace
+ readonly property bool _currentMonitor: widgetData?.runningAppsCurrentMonitor !== undefined ? widgetData.runningAppsCurrentMonitor : SettingsData.runningAppsCurrentMonitor
+ readonly property bool _groupByApp: widgetData?.runningAppsGroupByApp !== undefined ? widgetData.runningAppsGroupByApp : SettingsData.runningAppsGroupByApp
+
+ readonly property var sortedToplevels: {
+ _toplevelsUpdateTrigger;
+ let toplevels = CompositorService.sortedToplevels;
+ if (!toplevels || toplevels.length === 0)
+ return [];
+
+ if (_currentWorkspace)
+ toplevels = CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || [];
+ if (_currentMonitor)
+ toplevels = CompositorService.filterCurrentDisplay(toplevels, parentScreen?.name) || [];
+ return toplevels;
+ }
+
+ Connections {
+ target: CompositorService
+ function onToplevelsChanged() {
+ _toplevelsUpdateTrigger++;
+ }
+ }
+
+ Connections {
+ target: DesktopEntries
+ function onApplicationsChanged() {
+ _desktopEntriesUpdateTrigger++;
+ }
+ }
+
+ Connections {
+ target: SettingsData
+ function onAppIdSubstitutionsChanged() {
+ _appIdSubstitutionsTrigger++;
+ }
+ }
+ readonly property var groupedWindows: {
+ if (!_groupByApp) {
+ return [];
+ }
+ try {
+ if (!sortedToplevels || sortedToplevels.length === 0) {
+ return [];
+ }
+ const appGroups = new Map();
+ sortedToplevels.forEach((toplevel, index) => {
+ if (!toplevel)
+ return;
+ const appId = toplevel?.appId || "unknown";
+ if (!appGroups.has(appId)) {
+ appGroups.set(appId, {
+ "appId": appId,
+ "windows": []
+ });
+ }
+ appGroups.get(appId).windows.push({
+ "toplevel": toplevel,
+ "windowId": index,
+ "windowTitle": toplevel?.title || "(Unnamed)"
+ });
+ });
+ return Array.from(appGroups.values());
+ } catch (e) {
+ return [];
+ }
+ }
+ readonly property int windowCount: _groupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
+ readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6
+
+ readonly property string focusedAppId: {
+ if (!sortedToplevels || sortedToplevels.length === 0)
+ return "";
+ for (let i = 0; i < sortedToplevels.length; i++) {
+ if (sortedToplevels[i].activated)
+ return sortedToplevels[i].appId || "";
+ }
+ return "";
+ }
+
+ visible: windowCount > 0
+
+ property real scrollAccumulator: 0
+ property real touchpadThreshold: 500
+
+ onWheel: function (wheelEvent) {
+ wheelEvent.accepted = true;
+ const deltaY = wheelEvent.angleDelta.y;
+ const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
+
+ const windows = root.sortedToplevels;
+ if (windows.length < 2)
+ return;
+
+ if (isMouseWheel) {
+ let currentIndex = -1;
+ for (var i = 0; i < windows.length; i++) {
+ if (windows[i].activated) {
+ currentIndex = i;
+ break;
+ }
+ }
+
+ let nextIndex;
+ if (deltaY < 0) {
+ nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, windows.length - 1);
+ } else {
+ nextIndex = currentIndex === -1 ? windows.length - 1 : Math.max(currentIndex - 1, 0);
+ }
+
+ const nextWindow = windows[nextIndex];
+ if (nextWindow)
+ nextWindow.activate();
+ } else {
+ scrollAccumulator += deltaY;
+
+ if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
+ let currentIndex = -1;
+ for (var i = 0; i < windows.length; i++) {
+ if (windows[i].activated) {
+ currentIndex = i;
+ break;
+ }
+ }
+
+ let nextIndex;
+ if (scrollAccumulator < 0) {
+ nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, windows.length - 1);
+ } else {
+ nextIndex = currentIndex === -1 ? windows.length - 1 : Math.max(currentIndex - 1, 0);
+ }
+
+ const nextWindow = windows[nextIndex];
+ if (nextWindow)
+ nextWindow.activate();
+
+ scrollAccumulator = 0;
+ }
+ }
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
+ implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
+
+ Loader {
+ id: layoutLoader
+ anchors.centerIn: parent
+ sourceComponent: root.isVerticalOrientation ? columnLayout : rowLayout
+ }
+ }
+ }
+
+ Component {
+ id: rowLayout
+ Row {
+ spacing: Theme.spacingXS
+
+ Repeater {
+ id: windowRepeater
+ model: ScriptModel {
+ values: _groupByApp ? groupedWindows : sortedToplevels
+ objectProp: _groupByApp ? "appId" : "address"
+ }
+
+ delegate: Item {
+ id: delegateItem
+
+ property bool isGrouped: root._groupByApp
+ property var groupData: isGrouped ? modelData : null
+ property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
+ property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
+ property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
+ readonly property string effectiveAppId: {
+ root._appIdSubstitutionsTrigger;
+ return Paths.moddedAppId(appId);
+ }
+ property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
+ property var toplevelObject: toplevelData
+ property int windowCount: isGrouped ? modelData.windows.length : 1
+ property string tooltipText: {
+ root._desktopEntriesUpdateTrigger;
+ const desktopEntry = effectiveAppId ? DesktopEntries.heuristicLookup(effectiveAppId) : null;
+ const appName = effectiveAppId ? Paths.getAppName(effectiveAppId, desktopEntry) : "Unknown";
+
+ if (isGrouped && windowCount > 1) {
+ return appName + " (" + windowCount + " windows)";
+ }
+ return appName + (windowTitle ? " • " + windowTitle : "");
+ }
+ readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120)
+
+ width: visualWidth
+ height: root.barThickness
+
+ Rectangle {
+ id: visualContent
+ width: delegateItem.visualWidth
+ height: root.iconCellSize
+ anchors.centerIn: parent
+ radius: Theme.cornerRadius
+ color: {
+ if (isFocused) {
+ return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
+ }
+ return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
+ }
+
+ // App icon
+ IconImage {
+ id: iconImg
+ anchors.left: parent.left
+ anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)) / 2) : Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ source: {
+ root._desktopEntriesUpdateTrigger;
+ root._appIdSubstitutionsTrigger;
+ if (!effectiveAppId)
+ return "";
+ const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
+ return Paths.getAppIcon(effectiveAppId, desktopEntry);
+ }
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ visible: status === Image.Ready
+ layer.enabled: appId === "org.quickshell" || appId === "com.danklinux.dms"
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.primary
+ }
+ }
+
+ DankIcon {
+ anchors.left: parent.left
+ anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)) / 2) : Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ name: "sports_esports"
+ color: Theme.widgetTextColor
+ visible: !iconImg.visible && Paths.isSteamApp(effectiveAppId)
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: !iconImg.visible && !Paths.isSteamApp(effectiveAppId)
+ text: {
+ root._desktopEntriesUpdateTrigger;
+ if (!effectiveAppId)
+ return "?";
+ const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
+ const appName = Paths.getAppName(effectiveAppId, desktopEntry);
+ return appName.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+ }
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? -2 : 2
+ anchors.bottomMargin: -2
+ width: 14
+ height: 14
+ radius: 7
+ color: Theme.primary
+ visible: isGrouped && windowCount > 1
+ z: 10
+
+ StyledText {
+ anchors.centerIn: parent
+ text: windowCount > 9 ? "9+" : windowCount
+ font.pixelSize: 9
+ color: Theme.surface
+ }
+ }
+
+ StyledText {
+ anchors.left: iconImg.right
+ anchors.leftMargin: Theme.spacingXS
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !(widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
+ text: windowTitle
+ font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ }
+
+ DankRipple {
+ id: itemRipple
+ cornerRadius: Theme.cornerRadius
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onPressed: mouse => {
+ const pos = mapToItem(visualContent, mouse.x, mouse.y);
+ itemRipple.trigger(pos.x, pos.y);
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.LeftButton) {
+ if (isGrouped && windowCount > 1) {
+ let currentIndex = -1;
+ for (var i = 0; i < groupData.windows.length; i++) {
+ if (groupData.windows[i].toplevel.activated) {
+ currentIndex = i;
+ break;
+ }
+ }
+ const nextIndex = (currentIndex + 1) % groupData.windows.length;
+ groupData.windows[nextIndex].toplevel.activate();
+ } else if (toplevelObject) {
+ toplevelObject.activate();
+ }
+ } else if (mouse.button === Qt.RightButton) {
+ if (tooltipLoader.item) {
+ tooltipLoader.item.hide();
+ }
+ tooltipLoader.active = false;
+
+ windowContextMenuLoader.active = true;
+ if (windowContextMenuLoader.item) {
+ windowContextMenuLoader.item.currentWindow = toplevelObject;
+ // Pass bar context
+ windowContextMenuLoader.item.triggerBarConfig = root.barConfig;
+ windowContextMenuLoader.item.triggerBarPosition = root.axis.edge === "left" ? 2 : (root.axis.edge === "right" ? 3 : (root.axis.edge === "top" ? 0 : 1));
+ windowContextMenuLoader.item.triggerBarThickness = root.barThickness;
+ windowContextMenuLoader.item.triggerBarSpacing = root.barSpacing;
+ if (root.isVerticalOrientation) {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
+ const screenX = root.parentScreen ? root.parentScreen.x : 0;
+ const screenY = root.parentScreen ? root.parentScreen.y : 0;
+ const relativeY = globalPos.y - screenY;
+ // Add minTooltipY offset to account for top bar
+ const adjustedY = relativeY + root.minTooltipY;
+ const xPos = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
+ windowContextMenuLoader.item.showAt(xPos, adjustedY, true, root.axis?.edge);
+ } else {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0);
+ const screenX = root.parentScreen ? root.parentScreen.x : 0;
+ const relativeX = globalPos.x - screenX;
+ const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
+ const isBottom = root.axis?.edge === "bottom";
+ const yPos = isBottom ? (screenHeight - root.barThickness - root.barSpacing - 32 - Theme.spacingXS) : (root.barThickness + root.barSpacing + Theme.spacingXS);
+ windowContextMenuLoader.item.showAt(relativeX, yPos, false, root.axis?.edge);
+ }
+ }
+ } else if (mouse.button === Qt.MiddleButton) {
+ if (toplevelObject) {
+ if (typeof toplevelObject.close === "function") {
+ toplevelObject.close();
+ }
+ }
+ }
+ }
+ onEntered: {
+ root.hoveredItem = delegateItem;
+ tooltipLoader.active = true;
+ if (tooltipLoader.item) {
+ if (root.isVerticalOrientation) {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
+ const screenX = root.parentScreen ? root.parentScreen.x : 0;
+ const screenY = root.parentScreen ? root.parentScreen.y : 0;
+ const relativeY = globalPos.y - screenY;
+ const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
+ const isLeft = root.axis?.edge === "left";
+ const adjustedY = relativeY + root.minTooltipY;
+ const finalX = screenX + tooltipX;
+ tooltipLoader.item.show(delegateItem.tooltipText, finalX, adjustedY, root.parentScreen, isLeft, !isLeft);
+ } else {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
+ const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
+ const isBottom = root.axis?.edge === "bottom";
+ const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
+ tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
+ }
+ }
+ }
+ onExited: {
+ if (root.hoveredItem === delegateItem) {
+ root.hoveredItem = null;
+ if (tooltipLoader.item) {
+ tooltipLoader.item.hide();
+ }
+
+ tooltipLoader.active = false;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: columnLayout
+ Column {
+ spacing: Theme.spacingXS
+
+ Repeater {
+ id: windowRepeater
+ model: ScriptModel {
+ values: _groupByApp ? groupedWindows : sortedToplevels
+ objectProp: _groupByApp ? "appId" : "address"
+ }
+
+ delegate: Item {
+ id: delegateItem
+
+ property bool isGrouped: root._groupByApp
+ property var groupData: isGrouped ? modelData : null
+ property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
+ property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
+ property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
+ readonly property string effectiveAppId: {
+ root._appIdSubstitutionsTrigger;
+ return Paths.moddedAppId(appId);
+ }
+ property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
+ property var toplevelObject: toplevelData
+ property int windowCount: isGrouped ? modelData.windows.length : 1
+ property string tooltipText: {
+ root._desktopEntriesUpdateTrigger;
+ const desktopEntry = effectiveAppId ? DesktopEntries.heuristicLookup(effectiveAppId) : null;
+ const appName = effectiveAppId ? Paths.getAppName(effectiveAppId, desktopEntry) : "Unknown";
+
+ if (isGrouped && windowCount > 1) {
+ return appName + " (" + windowCount + " windows)";
+ }
+ return appName + (windowTitle ? " • " + windowTitle : "");
+ }
+ readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120)
+
+ width: root.barThickness
+ height: root.iconCellSize
+
+ Rectangle {
+ id: visualContent
+ width: delegateItem.visualWidth
+ height: root.iconCellSize
+ anchors.centerIn: parent
+ radius: Theme.cornerRadius
+ color: {
+ if (isFocused) {
+ return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
+ }
+ return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
+ }
+
+ IconImage {
+ id: iconImg
+ anchors.left: parent.left
+ anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)) / 2) : Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ source: {
+ root._desktopEntriesUpdateTrigger;
+ root._appIdSubstitutionsTrigger;
+ if (!effectiveAppId)
+ return "";
+ const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
+ return Paths.getAppIcon(effectiveAppId, desktopEntry);
+ }
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ visible: status === Image.Ready
+ layer.enabled: appId === "org.quickshell" || appId === "com.danklinux.dms"
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.primary
+ }
+ }
+
+ DankIcon {
+ anchors.left: parent.left
+ anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)) / 2) : Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ name: "sports_esports"
+ color: Theme.widgetTextColor
+ visible: !iconImg.visible && Paths.isSteamApp(effectiveAppId)
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: !iconImg.visible && !Paths.isSteamApp(effectiveAppId)
+ text: {
+ root._desktopEntriesUpdateTrigger;
+ if (!effectiveAppId)
+ return "?";
+ const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
+ const appName = Paths.getAppName(effectiveAppId, desktopEntry);
+ return appName.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+ }
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? -2 : 2
+ anchors.bottomMargin: -2
+ width: 14
+ height: 14
+ radius: 7
+ color: Theme.primary
+ visible: isGrouped && windowCount > 1
+ z: 10
+
+ StyledText {
+ anchors.centerIn: parent
+ text: windowCount > 9 ? "9+" : windowCount
+ font.pixelSize: 9
+ color: Theme.surface
+ }
+ }
+
+ StyledText {
+ anchors.left: iconImg.right
+ anchors.leftMargin: Theme.spacingXS
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !(widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
+ text: windowTitle
+ font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ }
+
+ DankRipple {
+ id: itemRipple
+ cornerRadius: Theme.cornerRadius
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onPressed: mouse => {
+ const pos = mapToItem(visualContent, mouse.x, mouse.y);
+ itemRipple.trigger(pos.x, pos.y);
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.LeftButton) {
+ if (isGrouped && windowCount > 1) {
+ let currentIndex = -1;
+ for (var i = 0; i < groupData.windows.length; i++) {
+ if (groupData.windows[i].toplevel.activated) {
+ currentIndex = i;
+ break;
+ }
+ }
+ const nextIndex = (currentIndex + 1) % groupData.windows.length;
+ groupData.windows[nextIndex].toplevel.activate();
+ } else if (toplevelObject) {
+ toplevelObject.activate();
+ }
+ } else if (mouse.button === Qt.RightButton) {
+ if (tooltipLoader.item) {
+ tooltipLoader.item.hide();
+ }
+ tooltipLoader.active = false;
+
+ windowContextMenuLoader.active = true;
+ if (windowContextMenuLoader.item) {
+ windowContextMenuLoader.item.currentWindow = toplevelObject;
+ // Pass bar context
+ windowContextMenuLoader.item.triggerBarConfig = root.barConfig;
+ windowContextMenuLoader.item.triggerBarPosition = root.axis.edge === "left" ? 2 : (root.axis.edge === "right" ? 3 : (root.axis.edge === "top" ? 0 : 1));
+ windowContextMenuLoader.item.triggerBarThickness = root.barThickness;
+ windowContextMenuLoader.item.triggerBarSpacing = root.barSpacing;
+ if (root.isVerticalOrientation) {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
+ const screenX = root.parentScreen ? root.parentScreen.x : 0;
+ const screenY = root.parentScreen ? root.parentScreen.y : 0;
+ const relativeY = globalPos.y - screenY;
+ // Add minTooltipY offset to account for top bar
+ const adjustedY = relativeY + root.minTooltipY;
+ const xPos = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
+ windowContextMenuLoader.item.showAt(xPos, adjustedY, true, root.axis?.edge);
+ } else {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0);
+ const screenX = root.parentScreen ? root.parentScreen.x : 0;
+ const relativeX = globalPos.x - screenX;
+ const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
+ const isBottom = root.axis?.edge === "bottom";
+ const yPos = isBottom ? (screenHeight - root.barThickness - root.barSpacing - 32 - Theme.spacingXS) : (root.barThickness + root.barSpacing + Theme.spacingXS);
+ windowContextMenuLoader.item.showAt(relativeX, yPos, false, root.axis?.edge);
+ }
+ }
+ }
+ }
+ onEntered: {
+ root.hoveredItem = delegateItem;
+ tooltipLoader.active = true;
+ if (tooltipLoader.item) {
+ if (root.isVerticalOrientation) {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
+ const screenX = root.parentScreen ? root.parentScreen.x : 0;
+ const screenY = root.parentScreen ? root.parentScreen.y : 0;
+ const relativeY = globalPos.y - screenY;
+ const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
+ const isLeft = root.axis?.edge === "left";
+ const adjustedY = relativeY + root.minTooltipY;
+ const finalX = screenX + tooltipX;
+ tooltipLoader.item.show(delegateItem.tooltipText, finalX, adjustedY, root.parentScreen, isLeft, !isLeft);
+ } else {
+ const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
+ const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
+ const isBottom = root.axis?.edge === "bottom";
+ const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
+ tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
+ }
+ }
+ }
+ onExited: {
+ if (root.hoveredItem === delegateItem) {
+ root.hoveredItem = null;
+ if (tooltipLoader.item) {
+ tooltipLoader.item.hide();
+ }
+
+ tooltipLoader.active = false;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: tooltipLoader
+
+ active: false
+
+ sourceComponent: DankTooltip {}
+ }
+
+ Loader {
+ id: windowContextMenuLoader
+ active: false
+ sourceComponent: PanelWindow {
+ id: contextMenuWindow
+
+ WindowBlur {
+ targetWindow: contextMenuWindow
+ blurX: contextMenuRect.x
+ blurY: contextMenuRect.y
+ blurWidth: contextMenuWindow.isVisible ? contextMenuRect.width : 0
+ blurHeight: contextMenuWindow.isVisible ? contextMenuRect.height : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ property var currentWindow: null
+ property bool isVisible: false
+ property point anchorPos: Qt.point(0, 0)
+ property bool isVertical: false
+ property string edge: "top"
+
+ // New properties for bar context
+ property int triggerBarPosition: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top)
+ property real triggerBarThickness: 0
+ property real triggerBarSpacing: 0
+ property var triggerBarConfig: null
+
+ readonly property real effectiveBarThickness: {
+ if (triggerBarThickness > 0 && triggerBarSpacing > 0) {
+ return triggerBarThickness + triggerBarSpacing;
+ }
+ return Math.max(26 + (barConfig?.innerPadding ?? 4) * 0.6, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))) + (barConfig?.spacing ?? 4);
+ }
+
+ property var barBounds: {
+ if (!contextMenuWindow.screen || !triggerBarConfig) {
+ return {
+ "x": 0,
+ "y": 0,
+ "width": 0,
+ "height": 0,
+ "wingSize": 0
+ };
+ }
+ return SettingsData.getBarBounds(contextMenuWindow.screen, effectiveBarThickness, triggerBarPosition, triggerBarConfig);
+ }
+
+ property real barY: barBounds.y
+
+ function showAt(x, y, vertical, barEdge) {
+ screen = root.parentScreen;
+ anchorPos = Qt.point(x, y);
+ isVertical = vertical ?? false;
+ edge = barEdge ?? "top";
+ isVisible = true;
+ visible = true;
+
+ if (screen) {
+ TrayMenuManager.registerMenu(screen.name, contextMenuWindow);
+ }
+ }
+
+ function close() {
+ isVisible = false;
+ visible = false;
+ windowContextMenuLoader.active = false;
+
+ if (screen) {
+ TrayMenuManager.unregisterMenu(screen.name);
+ }
+ }
+
+ implicitWidth: 100
+ implicitHeight: 40
+ visible: false
+ color: "transparent"
+
+ WlrLayershell.layer: WlrLayershell.Overlay
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ Component.onDestruction: {
+ if (screen) {
+ TrayMenuManager.unregisterMenu(screen.name);
+ }
+ }
+
+ Connections {
+ target: PopoutManager
+ function onPopoutOpening() {
+ contextMenuWindow.close();
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: contextMenuWindow.close()
+ }
+
+ Rectangle {
+ id: contextMenuRect
+ x: {
+ if (contextMenuWindow.isVertical) {
+ if (contextMenuWindow.edge === "left") {
+ return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x);
+ } else {
+ return Math.max(10, contextMenuWindow.anchorPos.x - width);
+ }
+ } else {
+ const left = 10;
+ const right = contextMenuWindow.width - width - 10;
+ const want = contextMenuWindow.anchorPos.x - width / 2;
+ return Math.max(left, Math.min(right, want));
+ }
+ }
+ y: {
+ if (contextMenuWindow.isVertical) {
+ const top = Math.max(barY, 10);
+ const bottom = contextMenuWindow.height - height - 10;
+ const want = contextMenuWindow.anchorPos.y - height / 2;
+ return Math.max(top, Math.min(bottom, want));
+ } else {
+ return contextMenuWindow.anchorPos.y;
+ }
+ }
+ width: 100
+ height: 32
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.width: BlurService.enabled ? BlurService.borderWidth : 1
+ border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: closeMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ text: I18n.tr("Close")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.widgetTextColor
+ }
+
+ MouseArea {
+ id: closeMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (contextMenuWindow.currentWindow) {
+ contextMenuWindow.currentWindow.close();
+ }
+ contextMenuWindow.close();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml
new file mode 100644
index 0000000..7fb9362
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml
@@ -0,0 +1,1893 @@
+import QtQuick
+import Quickshell
+import Quickshell.Hyprland
+import Quickshell.Services.SystemTray
+import Quickshell.Wayland
+import Quickshell.Widgets
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ enableBackgroundHover: false
+ enableCursor: false
+
+ property var parentWindow: null
+ property var widgetData: null
+ property string section: "right"
+ property bool isAtBottom: false
+ property bool isAutoHideBar: false
+ property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
+ readonly property var hiddenTrayIds: {
+ const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
+ return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
+ }
+ readonly property var allTrayItems: {
+ if (!hiddenTrayIds.length) {
+ return SystemTray.items.values;
+ }
+ return SystemTray.items.values.filter(item => {
+ const itemId = item?.id || "";
+ return !hiddenTrayIds.includes(itemId.toLowerCase());
+ });
+ }
+ function getTrayItemKey(item) {
+ const id = item?.id || "";
+ const tooltipTitle = item?.tooltipTitle || "";
+ if (!tooltipTitle || tooltipTitle === id) {
+ return id;
+ }
+ return `${id}::${tooltipTitle}`;
+ }
+
+ function trayIconSourceFor(trayItem) {
+ let icon = trayItem && trayItem.icon;
+ if (typeof icon === 'string' || icon instanceof String) {
+ if (icon === "")
+ return "";
+ if (icon.includes("?path=")) {
+ const split = icon.split("?path=");
+ if (split.length !== 2)
+ return icon;
+ const name = split[0];
+ const path = split[1];
+ let fileName = name.substring(name.lastIndexOf("/") + 1);
+ if (fileName.startsWith("dropboxstatus")) {
+ fileName = `hicolor/16x16/status/${fileName}`;
+ }
+ return `file://${path}/${fileName}`;
+ }
+ if (icon.startsWith("/") && !icon.startsWith("file://"))
+ return `file://${icon}`;
+ return icon;
+ }
+ return "";
+ }
+
+ function activateInlineTrayItem(trayItem, anchorItem) {
+ if (!trayItem)
+ return;
+ if (!trayItem.onlyMenu) {
+ trayItem.activate();
+ return;
+ }
+ if (!trayItem.hasMenu)
+ return;
+ root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
+ }
+
+ function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) {
+ if (!trayItem) {
+ return;
+ }
+ if (!trayItem.hasMenu) {
+ const gp = areaItem.mapToGlobal(mouse.x, mouse.y);
+ root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
+ return;
+ }
+ root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
+ }
+
+ function toggleIconName() {
+ const edge = root.axis?.edge;
+ if (root.useOverflowPopup) {
+ switch (edge) {
+ case "left":
+ return root.menuOpen ? "keyboard_arrow_left" : "keyboard_arrow_right";
+ case "right":
+ return root.menuOpen ? "keyboard_arrow_right" : "keyboard_arrow_left";
+ case "bottom":
+ return root.menuOpen ? "keyboard_arrow_down" : "keyboard_arrow_up";
+ case "top":
+ return root.menuOpen ? "keyboard_arrow_up" : "keyboard_arrow_down";
+ }
+ }
+
+ if (edge === "left" || edge === "right") {
+ return root.menuOpen == (root.section !== "right") ? "keyboard_arrow_up" : "keyboard_arrow_down";
+ }
+
+ return root.menuOpen != (root.section === "right") ? "keyboard_arrow_left" : "keyboard_arrow_right";
+ }
+
+ // ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
+ function callContextMenuFallback(trayItemId, globalX, globalY) {
+ const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
+ Quickshell.execDetached(["bash", "-c", script, "_", trayItemId, String(globalX), String(globalY)]);
+ }
+
+ property int _trayOrderTrigger: 0
+
+ Connections {
+ target: SessionData
+ function onTrayItemOrderChanged() {
+ root._trayOrderTrigger++;
+ }
+ }
+
+ function sortByPreferredOrder(items, trigger) {
+ void trigger;
+ const savedOrder = SessionData.trayItemOrder || [];
+ const orderMap = new Map();
+ savedOrder.forEach((key, idx) => orderMap.set(key, idx));
+
+ return [...items].sort((a, b) => {
+ const keyA = getTrayItemKey(a);
+ const keyB = getTrayItemKey(b);
+ const orderA = orderMap.has(keyA) ? orderMap.get(keyA) : 10000 + items.indexOf(a);
+ const orderB = orderMap.has(keyB) ? orderMap.get(keyB) : 10000 + items.indexOf(b);
+ return orderA - orderB;
+ });
+ }
+
+ readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger)
+ readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item))
+ readonly property var mainBarItemsRaw: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
+ readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({
+ key: getTrayItemKey(item),
+ item: item
+ }))
+ readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
+ readonly property bool reverseInlineHorizontal: !useOverflowPopup && !isVerticalOrientation && section === "right"
+ readonly property bool reverseInlineVertical: !useOverflowPopup && isVerticalOrientation && section === "right"
+ readonly property var displayedMainBarItems: reverseInlineHorizontal ? [...mainBarItems].reverse() : mainBarItems
+ readonly property var displayedInlineExpandedItems: (reverseInlineHorizontal ? [...hiddenBarItems].reverse() : hiddenBarItems).map(item => ({
+ key: getTrayItemKey(item),
+ item: item
+ }))
+
+ function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) {
+ if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0)
+ return;
+
+ const fromKey = mainBarItems[visibleFromIndex]?.key ?? null;
+ const toKey = mainBarItems[visibleToIndex]?.key ?? null;
+ if (!fromKey || !toKey)
+ return;
+
+ const fullOrder = [...allSortedTrayItemKeys];
+ const fullFromIndex = fullOrder.indexOf(fromKey);
+ const fullToIndex = fullOrder.indexOf(toKey);
+ if (fullFromIndex < 0 || fullToIndex < 0)
+ return;
+
+ const movedKey = fullOrder.splice(fullFromIndex, 1)[0];
+ fullOrder.splice(fullToIndex, 0, movedKey);
+ SessionData.setTrayItemOrder(fullOrder);
+ }
+
+ property int draggedIndex: -1
+ property int dropTargetIndex: -1
+ property bool suppressShiftAnimation: false
+ readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
+ readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
+ visible: allTrayItems.length > 0
+ opacity: allTrayItems.length > 0 ? 1 : 0
+
+ states: [
+ State {
+ name: "hidden_horizontal"
+ when: allTrayItems.length === 0 && !isVerticalOrientation
+ PropertyChanges {
+ target: root
+ width: 0
+ }
+ },
+ State {
+ name: "hidden_vertical"
+ when: allTrayItems.length === 0 && isVerticalOrientation
+ PropertyChanges {
+ target: root
+ height: 0
+ }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ NumberAnimation {
+ properties: "width,height"
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ ]
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ readonly property real trayItemSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6
+
+ readonly property real minTooltipY: {
+ if (!parentScreen || !isVerticalOrientation) {
+ return 0;
+ }
+
+ if (isAutoHideBar) {
+ return 0;
+ }
+
+ if (parentScreen.y > 0) {
+ const estimatedTopBarHeight = barThickness + barSpacing;
+ return estimatedTopBarHeight;
+ }
+
+ return 0;
+ }
+
+ readonly property string autoBarShadowDirection: {
+ const edge = root.axis?.edge;
+ switch (edge) {
+ case "top":
+ return "top";
+ case "bottom":
+ return "bottom";
+ case "left":
+ return "left";
+ case "right":
+ return "right";
+ default:
+ return "bottom";
+ }
+ }
+ readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
+
+ property bool menuOpen: false
+ property var currentTrayMenu: null
+
+ content: Component {
+ Item {
+ implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
+ implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
+
+ Loader {
+ id: layoutLoader
+ anchors.centerIn: parent
+ sourceComponent: root.isVerticalOrientation ? columnComp : rowComp
+ }
+ }
+ }
+
+ Component {
+ id: rowComp
+ Row {
+ spacing: 0
+ layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight
+
+ Repeater {
+ model: ScriptModel {
+ values: root.displayedMainBarItems
+ objectProp: "key"
+ }
+
+ delegate: Item {
+ id: delegateRoot
+ property var trayItem: modelData.item
+ property string itemKey: modelData.key
+ property string iconSource: root.trayIconSourceFor(trayItem)
+
+ width: root.trayItemSize
+ height: root.barThickness
+ z: dragHandler.dragging ? 100 : 0
+
+ property real shiftOffset: {
+ if (root.draggedIndex < 0)
+ return 0;
+ if (index === root.draggedIndex)
+ return 0;
+ const dragIdx = root.draggedIndex;
+ const dropIdx = root.dropTargetIndex;
+ const shiftAmount = root.trayItemSize;
+ if (dropIdx < 0)
+ return 0;
+ if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
+ return -shiftAmount;
+ if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
+ return shiftAmount;
+ return 0;
+ }
+
+ transform: Translate {
+ x: delegateRoot.shiftOffset
+ Behavior on x {
+ enabled: !root.suppressShiftAnimation
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ Item {
+ id: dragHandler
+ anchors.fill: parent
+ property bool dragging: false
+ property point dragStartPos: Qt.point(0, 0)
+ property real dragAxisOffset: 0
+ property bool longPressing: false
+
+ Timer {
+ id: longPressTimer
+ interval: 400
+ repeat: false
+ onTriggered: dragHandler.longPressing = true
+ }
+ }
+
+ Rectangle {
+ id: visualContent
+ width: root.trayItemSize
+ height: root.trayItemSize
+ anchors.centerIn: parent
+ radius: Theme.cornerRadius
+ color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+ border.width: dragHandler.dragging ? 2 : 0
+ border.color: Theme.primary
+ opacity: dragHandler.dragging ? 0.8 : 1.0
+
+ transform: Translate {
+ x: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
+ }
+
+ IconImage {
+ id: iconImg
+ anchors.centerIn: parent
+ width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ source: delegateRoot.iconSource
+ asynchronous: true
+ smooth: true
+ mipmap: true
+ visible: status === Image.Ready
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: !iconImg.visible
+ text: {
+ const itemId = trayItem?.id || "";
+ if (!itemId)
+ return "?";
+ return itemId.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+ }
+
+ DankRipple {
+ id: itemRipple
+ cornerRadius: Theme.cornerRadius
+ }
+ }
+
+ MouseArea {
+ id: trayItemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
+
+ onPressed: mouse => {
+ const pos = mapToItem(visualContent, mouse.x, mouse.y);
+ itemRipple.trigger(pos.x, pos.y);
+ if (mouse.button === Qt.LeftButton) {
+ dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
+ longPressTimer.start();
+ }
+ }
+
+ onReleased: mouse => {
+ longPressTimer.stop();
+ const wasDragging = dragHandler.dragging;
+ const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
+
+ if (didReorder) {
+ root.suppressShiftAnimation = true;
+ root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
+ Qt.callLater(() => root.suppressShiftAnimation = false);
+ }
+
+ dragHandler.longPressing = false;
+ dragHandler.dragging = false;
+ dragHandler.dragAxisOffset = 0;
+ root.draggedIndex = -1;
+ root.dropTargetIndex = -1;
+
+ if (wasDragging || mouse.button !== Qt.LeftButton)
+ return;
+
+ if (!delegateRoot.trayItem)
+ return;
+ if (!delegateRoot.trayItem.onlyMenu) {
+ delegateRoot.trayItem.activate();
+ return;
+ }
+ if (!delegateRoot.trayItem.hasMenu)
+ return;
+ if (root.useOverflowPopup)
+ root.menuOpen = false;
+ root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
+ }
+
+ onPositionChanged: mouse => {
+ if (dragHandler.longPressing && !dragHandler.dragging) {
+ const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
+ if (distance > 5) {
+ dragHandler.dragging = true;
+ root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
+ root.dropTargetIndex = root.draggedIndex;
+ }
+ }
+ if (!dragHandler.dragging)
+ return;
+
+ const axisOffset = mouse.x - dragHandler.dragStartPos.x;
+ dragHandler.dragAxisOffset = axisOffset;
+ const itemSize = root.trayItemSize;
+ const slotOffset = Math.round(axisOffset / itemSize);
+ const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
+ const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
+ if (newTargetIndex !== root.dropTargetIndex) {
+ root.dropTargetIndex = newTargetIndex;
+ }
+ }
+
+ onClicked: mouse => {
+ if (dragHandler.dragging)
+ return;
+ if (mouse.button !== Qt.RightButton)
+ return;
+ if (!delegateRoot.trayItem?.hasMenu) {
+ const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
+ root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
+ return;
+ }
+ if (root.useOverflowPopup)
+ root.menuOpen = false;
+ root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
+ }
+ }
+ }
+ }
+
+ Item {
+ width: root.trayItemSize
+ height: root.barThickness
+ visible: root.hasHiddenItems
+
+ Rectangle {
+ id: caretButton
+ width: root.trayItemSize
+ height: root.trayItemSize
+ anchors.centerIn: parent
+ radius: Theme.cornerRadius
+ color: caretArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.toggleIconName()
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetTextColor
+ }
+
+ DankRipple {
+ id: caretRipple
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: caretArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ caretRipple.trigger(mouse.x, mouse.y);
+ }
+ onClicked: root.menuOpen = !root.menuOpen
+ }
+ }
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: root.displayedInlineExpandedItems
+ objectProp: "key"
+ }
+
+ delegate: inlineExpandedTrayItemDelegate
+ }
+ }
+ }
+
+ Component {
+ id: inlineExpandedTrayItemDelegate
+
+ Item {
+ property var trayItem: modelData.item
+ property string itemKey: modelData.key
+ property string iconSource: root.trayIconSourceFor(trayItem)
+
+ width: root.isVerticalOrientation ? root.barThickness : (root.inlineExpanded ? root.trayItemSize : 0)
+ height: root.isVerticalOrientation ? (root.inlineExpanded ? root.trayItemSize : 0) : root.barThickness
+ visible: width > 0 || height > 0
+
+ Behavior on width {
+ enabled: !root.isVerticalOrientation
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on height {
+ enabled: root.isVerticalOrientation
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Rectangle {
+ id: inlineVisualContent
+ width: root.trayItemSize
+ height: root.trayItemSize
+ x: root.isVerticalOrientation ? Math.round((parent.width - width) / 2) : (root.reverseInlineHorizontal ? parent.width - width : 0)
+ y: root.isVerticalOrientation ? (root.reverseInlineVertical ? parent.height - height : 0) : Math.round((parent.height - height) / 2)
+ radius: Theme.cornerRadius
+ color: inlineTrayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+ opacity: root.inlineExpanded ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ IconImage {
+ id: inlineIconImg
+ anchors.centerIn: parent
+ width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ source: iconSource
+ asynchronous: true
+ smooth: true
+ mipmap: true
+ visible: status === Image.Ready
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: !inlineIconImg.visible
+ text: {
+ const itemId = trayItem?.id || "";
+ if (!itemId)
+ return "?";
+ return itemId.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+ }
+
+ DankRipple {
+ id: inlineItemRipple
+ cornerRadius: Theme.cornerRadius
+ }
+ }
+
+ MouseArea {
+ id: inlineTrayItemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: Qt.PointingHandCursor
+ enabled: root.inlineExpanded
+
+ onPressed: mouse => {
+ const pos = mapToItem(inlineVisualContent, mouse.x, mouse.y);
+ inlineItemRipple.trigger(pos.x, pos.y);
+ }
+
+ onClicked: mouse => {
+ if (mouse.button === Qt.LeftButton) {
+ root.activateInlineTrayItem(trayItem, inlineVisualContent);
+ return;
+ }
+ if (mouse.button !== Qt.RightButton)
+ return;
+ root.openInlineTrayContextMenu(trayItem, inlineTrayItemArea, mouse, inlineVisualContent);
+ }
+ }
+ }
+ }
+
+ Component {
+ id: verticalMainTrayItemDelegate
+
+ Item {
+ property var trayItem: modelData.item
+ property string itemKey: modelData.key
+ property string iconSource: root.trayIconSourceFor(trayItem)
+
+ width: root.barThickness
+ height: root.trayItemSize
+ z: dragHandler.dragging ? 100 : 0
+
+ property real shiftOffset: {
+ if (root.draggedIndex < 0)
+ return 0;
+ if (index === root.draggedIndex)
+ return 0;
+ const dragIdx = root.draggedIndex;
+ const dropIdx = root.dropTargetIndex;
+ const shiftAmount = root.trayItemSize;
+ if (dropIdx < 0)
+ return 0;
+ if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
+ return -shiftAmount;
+ if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
+ return shiftAmount;
+ return 0;
+ }
+
+ transform: Translate {
+ y: shiftOffset
+ Behavior on y {
+ enabled: !root.suppressShiftAnimation
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ Item {
+ id: dragHandler
+ anchors.fill: parent
+ property bool dragging: false
+ property point dragStartPos: Qt.point(0, 0)
+ property real dragAxisOffset: 0
+ property bool longPressing: false
+
+ Timer {
+ id: longPressTimer
+ interval: 400
+ repeat: false
+ onTriggered: dragHandler.longPressing = true
+ }
+ }
+
+ Rectangle {
+ id: visualContent
+ width: root.trayItemSize
+ height: root.trayItemSize
+ anchors.centerIn: parent
+ radius: Theme.cornerRadius
+ color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+ border.width: dragHandler.dragging ? 2 : 0
+ border.color: Theme.primary
+ opacity: dragHandler.dragging ? 0.8 : 1.0
+
+ transform: Translate {
+ y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
+ }
+
+ IconImage {
+ id: iconImg
+ anchors.centerIn: parent
+ width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ source: iconSource
+ asynchronous: true
+ smooth: true
+ mipmap: true
+ visible: status === Image.Ready
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: !iconImg.visible
+ text: {
+ const itemId = trayItem?.id || "";
+ if (!itemId)
+ return "?";
+ return itemId.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+ }
+
+ DankRipple {
+ id: itemRipple
+ cornerRadius: Theme.cornerRadius
+ }
+ }
+
+ MouseArea {
+ id: trayItemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
+
+ onPressed: mouse => {
+ const pos = mapToItem(visualContent, mouse.x, mouse.y);
+ itemRipple.trigger(pos.x, pos.y);
+ if (mouse.button === Qt.LeftButton) {
+ dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
+ longPressTimer.start();
+ }
+ }
+
+ onReleased: mouse => {
+ longPressTimer.stop();
+ const wasDragging = dragHandler.dragging;
+ const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
+
+ if (didReorder) {
+ root.suppressShiftAnimation = true;
+ root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
+ Qt.callLater(() => root.suppressShiftAnimation = false);
+ }
+
+ dragHandler.longPressing = false;
+ dragHandler.dragging = false;
+ dragHandler.dragAxisOffset = 0;
+ root.draggedIndex = -1;
+ root.dropTargetIndex = -1;
+
+ if (wasDragging || mouse.button !== Qt.LeftButton)
+ return;
+
+ if (!trayItem)
+ return;
+ if (!trayItem.onlyMenu) {
+ trayItem.activate();
+ return;
+ }
+ if (!trayItem.hasMenu)
+ return;
+ if (root.useOverflowPopup)
+ root.menuOpen = false;
+ root.showForTrayItem(trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
+ }
+
+ onPositionChanged: mouse => {
+ if (dragHandler.longPressing && !dragHandler.dragging) {
+ const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
+ if (distance > 5) {
+ dragHandler.dragging = true;
+ root.draggedIndex = index;
+ root.dropTargetIndex = root.draggedIndex;
+ }
+ }
+ if (!dragHandler.dragging)
+ return;
+
+ const axisOffset = mouse.y - dragHandler.dragStartPos.y;
+ dragHandler.dragAxisOffset = axisOffset;
+ const itemSize = root.trayItemSize;
+ const slotOffset = Math.round(axisOffset / itemSize);
+ const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
+ if (newTargetIndex !== root.dropTargetIndex) {
+ root.dropTargetIndex = newTargetIndex;
+ }
+ }
+
+ onClicked: mouse => {
+ if (dragHandler.dragging)
+ return;
+ if (mouse.button !== Qt.RightButton)
+ return;
+ root.openInlineTrayContextMenu(trayItem, trayItemArea, mouse, visualContent);
+ }
+ }
+ }
+ }
+
+ Component {
+ id: columnComp
+ Column {
+ spacing: 0
+
+ // Column lacks layoutDirection, so we use four repeaters with mutually exclusive models to control whether main items or expanded items appear above/ below the toggle button.
+ // When reverseInlineVertical is true the first and third repeaters are empty and the second and fourth are active, and vice-versa.
+ // Because items are swapped between repeaters rather than reversed within a single list, vertical drag-and-drop indices don't need remapping (unlike the horizontal RightToLeft case).
+ Repeater {
+ model: ScriptModel {
+ values: root.reverseInlineVertical ? [] : root.displayedMainBarItems
+ objectProp: "key"
+ }
+ delegate: verticalMainTrayItemDelegate
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : []
+ objectProp: "key"
+ }
+ delegate: inlineExpandedTrayItemDelegate
+ }
+
+ Item {
+ width: root.barThickness
+ height: root.trayItemSize
+ visible: root.hasHiddenItems
+
+ Rectangle {
+ id: caretButtonVert
+ width: root.trayItemSize
+ height: root.trayItemSize
+ anchors.centerIn: parent
+ radius: Theme.cornerRadius
+ color: caretAreaVert.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.toggleIconName()
+ size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetTextColor
+ }
+
+ DankRipple {
+ id: caretRippleVert
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: caretAreaVert
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ caretRippleVert.trigger(mouse.x, mouse.y);
+ }
+ onClicked: root.menuOpen = !root.menuOpen
+ }
+ }
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: root.reverseInlineVertical ? [] : root.displayedInlineExpandedItems
+ objectProp: "key"
+ }
+ delegate: inlineExpandedTrayItemDelegate
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: root.reverseInlineVertical ? root.displayedMainBarItems : []
+ objectProp: "key"
+ }
+ delegate: verticalMainTrayItemDelegate
+ }
+ }
+ }
+
+ PanelWindow {
+ id: overflowMenu
+
+ WindowBlur {
+ targetWindow: overflowMenu
+ blurX: menuContainer.x
+ blurY: menuContainer.y
+ blurWidth: root.menuOpen ? menuContainer.width : 0
+ blurHeight: root.menuOpen ? menuContainer.height : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ visible: root.useOverflowPopup && root.menuOpen
+ screen: root.parentScreen
+ WlrLayershell.layer: WlrLayershell.Top
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: {
+ if (!root.menuOpen)
+ return WlrKeyboardFocus.None;
+ if (CompositorService.useHyprlandFocusGrab)
+ return WlrKeyboardFocus.OnDemand;
+ return WlrKeyboardFocus.Exclusive;
+ }
+ WlrLayershell.namespace: "dms:tray-overflow-menu"
+ color: "transparent"
+
+ HyprlandFocusGrab {
+ windows: [overflowMenu]
+ active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
+ }
+
+ Connections {
+ target: PopoutManager
+ function onPopoutOpening() {
+ if (root.useOverflowPopup)
+ root.menuOpen = false;
+ }
+ }
+
+ Component.onDestruction: {
+ if (root.parentScreen) {
+ TrayMenuManager.unregisterMenu(root.parentScreen.name);
+ }
+ }
+
+ function close() {
+ root.menuOpen = false;
+ }
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ readonly property real dpr: (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? CompositorService.getScreenScale(overflowMenu.screen) : (screen?.devicePixelRatio || 1)
+ property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
+
+ property var barBounds: {
+ if (!overflowMenu.screen || !root.barConfig) {
+ return {
+ "x": 0,
+ "y": 0,
+ "width": 0,
+ "height": 0,
+ "wingSize": 0
+ };
+ }
+ const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
+ return SettingsData.getBarBounds(overflowMenu.screen, root.barThickness + root.barSpacing, barPosition, root.barConfig);
+ }
+
+ property real barX: barBounds.x
+ property real barY: barBounds.y
+ property real barWidth: barBounds.width
+ property real barHeight: barBounds.height
+
+ readonly property int barPosition: root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1))
+ readonly property var adjacentBarInfo: parentScreen ? SettingsData.getAdjacentBarInfo(parentScreen, barPosition, root.barConfig) : ({
+ "topBar": 0,
+ "bottomBar": 0,
+ "leftBar": 0,
+ "rightBar": 0
+ })
+ readonly property real effectiveBarSize: root.barThickness + root.barSpacing
+
+ readonly property real maskX: {
+ const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
+ const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
+ return Math.max(triggeringBarX, adjacentLeftBar);
+ }
+
+ readonly property real maskY: {
+ const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
+ const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
+ return Math.max(triggeringBarY, adjacentTopBar);
+ }
+
+ readonly property real maskWidth: {
+ const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
+ const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
+ const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
+ return Math.max(100, width - maskX - rightExclusion);
+ }
+
+ readonly property real maskHeight: {
+ const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
+ const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
+ const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
+ return Math.max(100, height - maskY - bottomExclusion);
+ }
+
+ mask: Region {
+ item: Rectangle {
+ x: overflowMenu.maskX
+ y: overflowMenu.maskY
+ width: overflowMenu.maskWidth
+ height: overflowMenu.maskHeight
+ }
+ }
+
+ onVisibleChanged: {
+ if (visible) {
+ if (currentTrayMenu) {
+ currentTrayMenu.showMenu = false;
+ }
+ if (root.parentScreen) {
+ TrayMenuManager.registerMenu(root.parentScreen.name, overflowMenu);
+ }
+ PopoutManager.closeAllPopouts();
+ ModalManager.closeAllModalsExcept(null);
+ updatePosition();
+ } else if (!visible && root.parentScreen) {
+ TrayMenuManager.unregisterMenu(root.parentScreen.name);
+ }
+ }
+
+ MouseArea {
+ x: overflowMenu.maskX
+ y: overflowMenu.maskY
+ width: overflowMenu.maskWidth
+ height: overflowMenu.maskHeight
+ z: -1
+ enabled: root.menuOpen
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onClicked: mouse => {
+ const clickX = mouse.x + overflowMenu.maskX;
+ const clickY = mouse.y + overflowMenu.maskY;
+ const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height;
+
+ if (!outsideContent)
+ return;
+ root.menuOpen = false;
+ }
+ }
+
+ FocusScope {
+ id: overflowFocusScope
+ anchors.fill: parent
+ focus: true
+
+ Keys.onEscapePressed: {
+ root.menuOpen = false;
+ }
+ }
+
+ function updatePosition() {
+ const globalPos = root.mapToGlobal(0, 0);
+ const screenX = screen.x || 0;
+ const screenY = screen.y || 0;
+ const relativeX = globalPos.x - screenX;
+ const relativeY = globalPos.y - screenY;
+
+ if (root.isVerticalOrientation) {
+ const edge = root.axis?.edge;
+ let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
+ const adjustedY = relativeY + root.height / 2 + root.minTooltipY;
+ anchorPos = Qt.point(targetX, adjustedY);
+ } else {
+ let targetY = root.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
+ anchorPos = Qt.point(relativeX + root.width / 2, targetY);
+ }
+ }
+
+ Item {
+ id: menuContainer
+ objectName: "overflowMenuContainer"
+
+ readonly property real rawWidth: {
+ const itemCount = root.hiddenBarItems.length;
+ const cols = Math.min(5, itemCount);
+ const itemSize = root.trayItemSize + 4;
+ const spacing = 2;
+ return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2;
+ }
+ readonly property real rawHeight: {
+ const itemCount = root.hiddenBarItems.length;
+ const cols = Math.min(5, itemCount);
+ const rows = Math.ceil(itemCount / cols);
+ const itemSize = root.trayItemSize + 4;
+ const spacing = 2;
+ return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2;
+ }
+
+ readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr)
+ readonly property real alignedHeight: Theme.px(rawHeight, overflowMenu.dpr)
+
+ width: alignedWidth
+ height: alignedHeight
+
+ x: Theme.snap((() => {
+ if (root.isVerticalOrientation) {
+ const edge = root.axis?.edge;
+ if (edge === "left") {
+ const targetX = overflowMenu.anchorPos.x;
+ return Math.min(overflowMenu.screen.width - alignedWidth - 10, targetX);
+ } else {
+ const targetX = overflowMenu.anchorPos.x - alignedWidth;
+ return Math.max(10, targetX);
+ }
+ } else {
+ const left = 10;
+ const right = overflowMenu.width - alignedWidth - 10;
+ const want = overflowMenu.anchorPos.x - alignedWidth / 2;
+ return Math.max(left, Math.min(right, want));
+ }
+ })(), overflowMenu.dpr)
+
+ y: Theme.snap((() => {
+ if (root.isVerticalOrientation) {
+ const top = Math.max(overflowMenu.barY, 10);
+ const bottom = overflowMenu.height - alignedHeight - 10;
+ const want = overflowMenu.anchorPos.y - alignedHeight / 2;
+ return Math.max(top, Math.min(bottom, want));
+ } else {
+ if (root.isAtBottom) {
+ const targetY = overflowMenu.anchorPos.y - alignedHeight;
+ return Math.max(10, targetY);
+ } else {
+ const targetY = overflowMenu.anchorPos.y;
+ return Math.min(overflowMenu.screen.height - alignedHeight - 10, targetY);
+ }
+ }
+ })(), overflowMenu.dpr)
+
+ opacity: root.menuOpen ? 1 : 0
+ scale: root.menuOpen ? 1 : 0.85
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ ElevationShadow {
+ id: bgShadowLayer
+ anchors.fill: parent
+ level: Theme.elevationLevel3
+ direction: root.effectiveShadowDirection
+ fallbackOffset: 6
+ targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ targetRadius: Theme.cornerRadius
+ sourceRect.antialiasing: true
+ sourceRect.smooth: true
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
+ layer.smooth: true
+ layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
+ layer.textureMirroring: ShaderEffectSource.MirrorVertically
+ layer.samples: 4
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ radius: Theme.cornerRadius
+ border.color: BlurService.borderColor
+ border.width: BlurService.borderWidth
+ z: 100
+ }
+
+ Grid {
+ id: menuGrid
+ anchors.centerIn: parent
+ columns: Math.min(5, root.hiddenBarItems.length)
+ spacing: 2
+ rowSpacing: 2
+
+ Repeater {
+ model: root.hiddenBarItems
+
+ delegate: Rectangle {
+ property var trayItem: modelData
+ property string iconSource: root.trayIconSourceFor(trayItem)
+
+ width: root.trayItemSize + 4
+ height: root.trayItemSize + 4
+ radius: Theme.cornerRadius
+ color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
+
+ IconImage {
+ id: menuIconImg
+ anchors.centerIn: parent
+ width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ source: parent.iconSource
+ asynchronous: true
+ smooth: true
+ mipmap: true
+ visible: status === Image.Ready
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: !menuIconImg.visible
+ text: {
+ const itemId = trayItem?.id || "";
+ if (!itemId)
+ return "?";
+ return itemId.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+ }
+
+ MouseArea {
+ id: itemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: Qt.PointingHandCursor
+ onClicked: mouse => {
+ if (!trayItem)
+ return;
+ if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
+ trayItem.activate();
+ root.menuOpen = false;
+ return;
+ }
+ if (!trayItem.hasMenu) {
+ const gp = itemArea.mapToGlobal(mouse.x, mouse.y);
+ root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
+ return;
+ }
+ root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: trayMenuComponent
+
+ Rectangle {
+ id: menuRoot
+
+ property var trayItem: null
+ property var anchorItem: null
+ property var parentScreen: null
+ property bool isAtBottom: false
+ property bool isVertical: false
+ property var axis: null
+ property bool showMenu: false
+ property var menuHandle: null
+
+ ListModel {
+ id: entryStack
+ }
+ function topEntry() {
+ return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
+ }
+
+ function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
+ trayItem = item;
+ anchorItem = anchor;
+ parentScreen = screen;
+ isAtBottom = atBottom;
+ isVertical = vertical;
+ axis = axisObj;
+ menuHandle = item?.menu;
+
+ if (parentScreen) {
+ for (var i = 0; i < Quickshell.screens.length; i++) {
+ const s = Quickshell.screens[i];
+ if (s === parentScreen) {
+ menuWindow.screen = s;
+ break;
+ }
+ }
+ }
+
+ showMenu = true;
+ }
+
+ function close() {
+ showMenu = false;
+ }
+
+ Connections {
+ target: menuWindow
+ function onVisibleChanged() {
+ if (menuWindow.visible && parentScreen) {
+ TrayMenuManager.registerMenu(parentScreen.name, menuRoot);
+ } else if (!menuWindow.visible && parentScreen) {
+ TrayMenuManager.unregisterMenu(parentScreen.name);
+ }
+ }
+ }
+
+ Component.onDestruction: {
+ if (parentScreen) {
+ TrayMenuManager.unregisterMenu(parentScreen.name);
+ }
+ }
+
+ Connections {
+ target: PopoutManager
+ function onPopoutOpening() {
+ menuRoot.close();
+ }
+ }
+
+ function closeWithAction() {
+ close();
+ }
+
+ function showSubMenu(entry) {
+ if (!entry || !entry.hasChildren)
+ return;
+
+ entryStack.append({
+ handle: entry
+ });
+
+ const h = entry.menu || entry;
+ if (h && typeof h.updateLayout === "function")
+ h.updateLayout();
+
+ submenuHydrator.menu = h;
+ submenuHydrator.open();
+ Qt.callLater(() => submenuHydrator.close());
+ }
+
+ function goBack() {
+ if (!entryStack.count)
+ return;
+ entryStack.remove(entryStack.count - 1);
+ }
+
+ width: 0
+ height: 0
+ color: "transparent"
+
+ PanelWindow {
+ id: menuWindow
+
+ WindowBlur {
+ targetWindow: menuWindow
+ blurX: trayMenuContainer.x
+ blurY: trayMenuContainer.y
+ blurWidth: menuRoot.showMenu ? trayMenuContainer.width : 0
+ blurHeight: menuRoot.showMenu ? trayMenuContainer.height : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ WlrLayershell.namespace: "dms:tray-menu-window"
+ visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
+ WlrLayershell.layer: WlrLayershell.Top
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: {
+ if (!menuRoot.showMenu)
+ return WlrKeyboardFocus.None;
+ if (CompositorService.useHyprlandFocusGrab)
+ return WlrKeyboardFocus.OnDemand;
+ return WlrKeyboardFocus.Exclusive;
+ }
+ color: "transparent"
+
+ HyprlandFocusGrab {
+ windows: [menuWindow]
+ active: CompositorService.useHyprlandFocusGrab && menuRoot.showMenu
+ }
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ readonly property real dpr: (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? CompositorService.getScreenScale(menuWindow.screen) : (screen?.devicePixelRatio || 1)
+ property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
+
+ property var barBounds: {
+ if (!menuWindow.screen || !root.barConfig) {
+ return {
+ "x": 0,
+ "y": 0,
+ "width": 0,
+ "height": 0,
+ "wingSize": 0
+ };
+ }
+ const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
+ return SettingsData.getBarBounds(menuWindow.screen, root.barThickness + root.barSpacing, barPosition, root.barConfig);
+ }
+
+ property real barX: barBounds.x
+ property real barY: barBounds.y
+ property real barWidth: barBounds.width
+ property real barHeight: barBounds.height
+
+ readonly property int barPosition: root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1))
+ readonly property var adjacentBarInfo: menuRoot.parentScreen ? SettingsData.getAdjacentBarInfo(menuRoot.parentScreen, barPosition, root.barConfig) : ({
+ "topBar": 0,
+ "bottomBar": 0,
+ "leftBar": 0,
+ "rightBar": 0
+ })
+ readonly property real effectiveBarSize: root.barThickness + root.barSpacing
+
+ readonly property real maskX: {
+ const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
+ const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
+ return Math.max(triggeringBarX, adjacentLeftBar);
+ }
+
+ readonly property real maskY: {
+ const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
+ const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
+ return Math.max(triggeringBarY, adjacentTopBar);
+ }
+
+ readonly property real maskWidth: {
+ const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
+ const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
+ const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
+ return Math.max(100, width - maskX - rightExclusion);
+ }
+
+ readonly property real maskHeight: {
+ const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
+ const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
+ const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
+ return Math.max(100, height - maskY - bottomExclusion);
+ }
+
+ mask: Region {
+ item: Rectangle {
+ x: menuWindow.maskX
+ y: menuWindow.maskY
+ width: menuWindow.maskWidth
+ height: menuWindow.maskHeight
+ }
+ }
+
+ onVisibleChanged: {
+ if (visible) {
+ updatePosition();
+ if (root.useOverflowPopup)
+ root.menuOpen = false;
+ PopoutManager.closeAllPopouts();
+ ModalManager.closeAllModalsExcept(null);
+ }
+ }
+
+ MouseArea {
+ x: menuWindow.maskX
+ y: menuWindow.maskY
+ width: menuWindow.maskWidth
+ height: menuWindow.maskHeight
+ z: -1
+ enabled: menuRoot.showMenu
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onClicked: mouse => {
+ const clickX = mouse.x + menuWindow.maskX;
+ const clickY = mouse.y + menuWindow.maskY;
+ const outsideContent = clickX < trayMenuContainer.x || clickX > trayMenuContainer.x + trayMenuContainer.width || clickY < trayMenuContainer.y || clickY > trayMenuContainer.y + trayMenuContainer.height;
+
+ if (!outsideContent)
+ return;
+ menuRoot.close();
+ }
+ }
+
+ FocusScope {
+ id: menuFocusScope
+ anchors.fill: parent
+ focus: true
+
+ Keys.onEscapePressed: {
+ if (entryStack.count > 0) {
+ menuRoot.goBack();
+ } else {
+ menuRoot.close();
+ }
+ }
+ }
+
+ function updatePosition() {
+ const targetItem = (typeof menuRoot !== "undefined" && menuRoot.anchorItem) ? menuRoot.anchorItem : root;
+
+ const isFromOverflowMenu = targetItem.objectName === "overflowMenuContainer";
+
+ if (isFromOverflowMenu) {
+ if (menuRoot.isVertical) {
+ const edge = menuRoot.axis?.edge;
+ let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
+ const targetY = targetItem.y + targetItem.height / 2;
+ anchorPos = Qt.point(targetX, targetY);
+ } else {
+ const targetX = targetItem.x + targetItem.width / 2;
+ let targetY = menuRoot.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
+ anchorPos = Qt.point(targetX, targetY);
+ }
+ } else {
+ const globalPos = targetItem.mapToGlobal(0, 0);
+ const screenX = screen.x || 0;
+ const screenY = screen.y || 0;
+ const relativeX = globalPos.x - screenX;
+ const relativeY = globalPos.y - screenY;
+
+ if (menuRoot.isVertical) {
+ const edge = menuRoot.axis?.edge;
+ let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
+ const adjustedY = relativeY + targetItem.height / 2 + root.minTooltipY;
+ anchorPos = Qt.point(targetX, adjustedY);
+ } else {
+ let targetY = menuRoot.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
+ anchorPos = Qt.point(relativeX + targetItem.width / 2, targetY);
+ }
+ }
+ }
+
+ Item {
+ id: trayMenuContainer
+
+ readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
+ readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
+
+ readonly property real alignedWidth: Theme.px(rawWidth, menuWindow.dpr)
+ readonly property real alignedHeight: Theme.px(rawHeight, menuWindow.dpr)
+
+ width: alignedWidth
+ height: alignedHeight
+
+ x: Theme.snap((() => {
+ if (menuRoot.isVertical) {
+ const edge = menuRoot.axis?.edge;
+ if (edge === "left") {
+ const targetX = menuWindow.anchorPos.x;
+ return Math.min(menuWindow.screen.width - alignedWidth - 10, targetX);
+ } else {
+ const targetX = menuWindow.anchorPos.x - alignedWidth;
+ return Math.max(10, targetX);
+ }
+ } else {
+ const left = 10;
+ const right = menuWindow.width - alignedWidth - 10;
+ const want = menuWindow.anchorPos.x - alignedWidth / 2;
+ return Math.max(left, Math.min(right, want));
+ }
+ })(), menuWindow.dpr)
+
+ y: Theme.snap((() => {
+ if (menuRoot.isVertical) {
+ const top = Math.max(menuWindow.barY, 10);
+ const bottom = menuWindow.height - alignedHeight - 10;
+ const want = menuWindow.anchorPos.y - alignedHeight / 2;
+ return Math.max(top, Math.min(bottom, want));
+ } else {
+ if (menuRoot.isAtBottom) {
+ const targetY = menuWindow.anchorPos.y - alignedHeight;
+ return Math.max(10, targetY);
+ } else {
+ const targetY = menuWindow.anchorPos.y;
+ return Math.min(menuWindow.screen.height - alignedHeight - 10, targetY);
+ }
+ }
+ })(), menuWindow.dpr)
+
+ opacity: menuRoot.showMenu ? 1 : 0
+ scale: menuRoot.showMenu ? 1 : 0.85
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ ElevationShadow {
+ id: menuBgShadowLayer
+ anchors.fill: parent
+ level: Theme.elevationLevel3
+ direction: root.effectiveShadowDirection
+ fallbackOffset: 6
+ targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ targetRadius: Theme.cornerRadius
+ sourceRect.antialiasing: true
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
+ layer.smooth: true
+ layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
+ layer.textureMirroring: ShaderEffectSource.MirrorVertically
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ radius: Theme.cornerRadius
+ border.color: BlurService.borderColor
+ border.width: BlurService.borderWidth
+ z: 100
+ }
+
+ QsMenuAnchor {
+ id: submenuHydrator
+ anchor.window: menuWindow
+ }
+
+ QsMenuOpener {
+ id: rootOpener
+ menu: menuRoot.menuHandle
+ }
+
+ QsMenuOpener {
+ id: subOpener
+ menu: {
+ const e = menuRoot.topEntry();
+ return e ? (e.menu || e) : null;
+ }
+ }
+
+ Column {
+ id: menuColumn
+
+ width: parent.width - Theme.spacingS * 2
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: Theme.spacingS
+ spacing: 1
+
+ Rectangle {
+ visible: entryStack.count === 0
+ width: parent.width
+ height: 28
+ radius: Theme.cornerRadius
+ color: visibilityToggleArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ text: menuRoot.trayItem?.id || "Unknown"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ elide: Text.ElideMiddle
+ width: parent.width - Theme.spacingS * 2 - 24
+ }
+
+ DankIcon {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ name: SessionData.isHiddenTrayId(root.getTrayItemKey(menuRoot.trayItem)) ? "visibility" : "visibility_off"
+ size: 16
+ color: Theme.widgetTextColor
+ }
+
+ MouseArea {
+ id: visibilityToggleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ const itemKey = root.getTrayItemKey(menuRoot.trayItem);
+ if (!itemKey)
+ return;
+ if (SessionData.isHiddenTrayId(itemKey)) {
+ SessionData.showTrayId(itemKey);
+ } else {
+ SessionData.hideTrayId(itemKey);
+ }
+ menuRoot.closeWithAction();
+ }
+ }
+ }
+
+ Rectangle {
+ visible: entryStack.count === 0
+ width: parent.width
+ height: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ }
+
+ Rectangle {
+ visible: entryStack.count > 0
+ width: parent.width
+ height: 28
+ radius: Theme.cornerRadius
+ color: backArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "arrow_back"
+ size: 16
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Back")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: backArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: menuRoot.goBack()
+ }
+ }
+
+ Rectangle {
+ visible: entryStack.count > 0
+ width: parent.width
+ height: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ }
+
+ Repeater {
+ model: entryStack.count ? (subOpener.children ? subOpener.children : (menuRoot.topEntry()?.children || [])) : rootOpener.children
+
+ Rectangle {
+ property var menuEntry: modelData
+
+ width: menuColumn.width
+ height: menuEntry?.isSeparator ? 1 : 28
+ radius: menuEntry?.isSeparator ? 0 : Theme.cornerRadius
+ color: {
+ if (menuEntry?.isSeparator)
+ return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
+ return itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0);
+ }
+
+ MouseArea {
+ id: itemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ enabled: !menuEntry?.isSeparator && (menuEntry?.enabled !== false)
+ cursorShape: Qt.PointingHandCursor
+
+ onClicked: {
+ if (!menuEntry || menuEntry.isSeparator)
+ return;
+ if (menuEntry.hasChildren) {
+ menuRoot.showSubMenu(menuEntry);
+ return;
+ }
+
+ if (typeof menuEntry.activate === "function") {
+ menuEntry.activate();
+ } else if (typeof menuEntry.triggered === "function") {
+ menuEntry.triggered();
+ }
+ Qt.createQmlObject('import QtQuick; Timer { interval: 80; running: true; repeat: false; onTriggered: menuRoot.closeWithAction() }', menuRoot);
+ }
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+ visible: !menuEntry?.isSeparator
+
+ Rectangle {
+ width: 16
+ height: 16
+ anchors.verticalCenter: parent.verticalCenter
+ visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0
+ radius: menuEntry?.buttonType === 2 ? 8 : 2
+ border.width: 1
+ border.color: Theme.outline
+ color: "transparent"
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: parent.width - 6
+ height: parent.height - 6
+ radius: parent.radius - 3
+ color: Theme.primary
+ visible: menuEntry?.checkState === 2
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "check"
+ size: 10
+ color: Theme.primaryText
+ visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2
+ }
+ }
+
+ Item {
+ width: 16
+ height: 16
+ anchors.verticalCenter: parent.verticalCenter
+ visible: (menuEntry?.icon ?? "") !== ""
+
+ Image {
+ anchors.fill: parent
+ source: menuEntry?.icon || ""
+ sourceSize.width: 16
+ sourceSize.height: 16
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+ }
+
+ StyledText {
+ text: menuEntry?.text || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: (menuEntry?.enabled !== false) ? Theme.surfaceText : Theme.surfaceTextMedium
+ elide: Text.ElideRight
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.max(150, parent.width - 64)
+ wrapMode: Text.NoWrap
+ }
+
+ Item {
+ width: 16
+ height: 16
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "chevron_right"
+ size: 14
+ color: Theme.widgetTextColor
+ visible: menuEntry?.hasChildren ?? false
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
+ if (!screen)
+ return;
+ if (currentTrayMenu) {
+ currentTrayMenu.showMenu = false;
+ currentTrayMenu.destroy();
+ currentTrayMenu = null;
+ }
+
+ PopoutManager.closeAllPopouts();
+ ModalManager.closeAllModalsExcept(null);
+
+ currentTrayMenu = trayMenuComponent.createObject(null);
+ if (!currentTrayMenu)
+ return;
+ currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj);
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml
new file mode 100644
index 0000000..696c76c
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml
@@ -0,0 +1,187 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ property bool isActive: false
+ readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
+ readonly property bool isChecking: SystemUpdateService.isChecking
+ readonly property bool shouldHide: SettingsData.updaterHideWidget && !hasUpdates && !isChecking && !SystemUpdateService.hasError
+
+ opacity: shouldHide ? 0 : 1
+
+ states: [
+ State {
+ name: "hidden_horizontal"
+ when: root.shouldHide && !isVerticalOrientation
+ PropertyChanges {
+ target: root
+ width: 0
+ }
+ },
+ State {
+ name: "hidden_vertical"
+ when: root.shouldHide && isVerticalOrientation
+ PropertyChanges {
+ target: root
+ height: 0
+ }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ NumberAnimation {
+ properties: "width,height"
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ ]
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Ref {
+ service: SystemUpdateService
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: root.isVerticalOrientation ? root.widgetThickness : updaterIcon.implicitWidth
+ implicitHeight: root.widgetThickness
+
+ DankIcon {
+ id: statusIcon
+ anchors.centerIn: parent
+ visible: root.isVerticalOrientation
+ name: {
+ if (root.isChecking)
+ return "refresh";
+ if (SystemUpdateService.hasError)
+ return "error";
+ if (root.hasUpdates)
+ return "system_update_alt";
+ return "check_circle";
+ }
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (SystemUpdateService.hasError)
+ return Theme.error;
+ if (root.hasUpdates)
+ return Theme.primary;
+ return root.isActive ? Theme.primary : Theme.surfaceText;
+ }
+
+ RotationAnimation {
+ id: rotationAnimation
+ target: statusIcon
+ property: "rotation"
+ from: 0
+ to: 360
+ duration: 1000
+ running: root.isChecking
+ loops: Animation.Infinite
+
+ onRunningChanged: {
+ if (!running) {
+ statusIcon.rotation = 0;
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: 8
+ height: 8
+ radius: 4
+ color: Theme.error
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.rightMargin: (barConfig?.removeWidgetPadding ?? false) ? 0 : 6
+ anchors.topMargin: (barConfig?.removeWidgetPadding ?? false) ? 0 : 6
+ visible: root.isVerticalOrientation && root.hasUpdates && !root.isChecking
+ }
+
+ Row {
+ id: updaterIcon
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+ visible: !root.isVerticalOrientation
+
+ DankIcon {
+ id: statusIconHorizontal
+ anchors.verticalCenter: parent.verticalCenter
+ name: {
+ if (root.isChecking)
+ return "refresh";
+ if (SystemUpdateService.hasError)
+ return "error";
+ if (root.hasUpdates)
+ return "system_update_alt";
+ return "check_circle";
+ }
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: {
+ if (SystemUpdateService.hasError)
+ return Theme.error;
+ if (root.hasUpdates)
+ return Theme.primary;
+ return root.isActive ? Theme.primary : Theme.surfaceText;
+ }
+
+ RotationAnimation {
+ id: rotationAnimationHorizontal
+ target: statusIconHorizontal
+ property: "rotation"
+ from: 0
+ to: 360
+ duration: 1000
+ running: root.isChecking
+ loops: Animation.Infinite
+
+ onRunningChanged: {
+ if (!running) {
+ statusIconHorizontal.rotation = 0;
+ }
+ }
+ }
+ }
+
+ StyledText {
+ id: countText
+ anchors.verticalCenter: parent.verticalCenter
+ text: SystemUpdateService.updateCount.toString()
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ visible: root.hasUpdates && !root.isChecking
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ z: 1
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ root.triggerRipple(this, mouse.x, mouse.y);
+ if (popoutTarget && popoutTarget.setTriggerPosition) {
+ const globalPos = root.visualContent.mapToItem(null, 0, 0);
+ const currentScreen = parentScreen || Screen;
+ const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth, root.barSpacing, barPosition, root.barConfig);
+ popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, root.barSpacing, root.barConfig);
+ }
+ root.clicked();
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Vpn.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Vpn.qml
new file mode 100644
index 0000000..fec33d0
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Vpn.qml
@@ -0,0 +1,141 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ Ref {
+ service: DMSNetworkService
+ }
+
+ property bool isHovered: clickArea.containsMouse
+ property bool isAutoHideBar: false
+
+ readonly property real minTooltipY: {
+ if (!parentScreen || !isVerticalOrientation) {
+ return 0;
+ }
+
+ if (isAutoHideBar) {
+ return 0;
+ }
+
+ if (parentScreen.y > 0) {
+ return barThickness + barSpacing;
+ }
+
+ return 0;
+ }
+
+ signal toggleVpnPopup
+
+ content: Component {
+ Item {
+ implicitWidth: icon.width
+ implicitHeight: root.widgetThickness - root.horizontalPadding * 2
+
+ DankIcon {
+ id: icon
+
+ name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
+ size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: DMSNetworkService.connected ? Theme.primary : Theme.widgetIconColor
+ opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
+ anchors.centerIn: parent
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: tooltipLoader
+ active: false
+ sourceComponent: DankTooltip {}
+ }
+
+ MouseArea {
+ id: clickArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ enabled: !DMSNetworkService.isBusy
+ onPressed: event => {
+ root.triggerRipple(this, event.x, event.y);
+ switch (event.button) {
+ case Qt.RightButton:
+ DMSNetworkService.toggleVpn();
+ return;
+ case Qt.LeftButton:
+ root.toggleVpnPopup();
+ return;
+ }
+ }
+ onEntered: {
+ if (!root.parentScreen || (popoutTarget?.shouldBeVisible))
+ return;
+ tooltipLoader.active = true;
+ if (!tooltipLoader.item)
+ return;
+ let tooltipText = "";
+ if (!DMSNetworkService.connected) {
+ tooltipText = "VPN Disconnected";
+ } else {
+ const names = DMSNetworkService.activeNames || [];
+ if (names.length <= 1) {
+ const name = names[0] || "";
+ const maxLength = 25;
+ const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name;
+ tooltipText = "VPN Connected • " + displayName;
+ } else {
+ const name = names[0];
+ const maxLength = 20;
+ const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name;
+ tooltipText = "VPN Connected • " + displayName + " +" + (names.length - 1);
+ }
+ }
+
+ if (root.isVerticalOrientation) {
+ const globalPos = mapToGlobal(width / 2, height / 2);
+ const currentScreen = root.parentScreen || Screen;
+ const screenX = currentScreen ? currentScreen.x : 0;
+ const screenY = currentScreen ? currentScreen.y : 0;
+ const relativeY = globalPos.y - screenY;
+ const adjustedY = relativeY + root.minTooltipY;
+ const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (currentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
+ const isLeft = root.axis?.edge === "left";
+ tooltipLoader.item.show(tooltipText, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft);
+ } else {
+ const isBottom = root.axis?.edge === "bottom";
+ const globalPos = mapToGlobal(width / 2, 0);
+ const currentScreen = root.parentScreen || Screen;
+
+ let tooltipY;
+ if (isBottom) {
+ const tooltipHeight = Theme.fontSizeSmall * 1.5 + Theme.spacingS * 2;
+ tooltipY = currentScreen.height - root.barThickness - root.barSpacing - Theme.spacingXS - tooltipHeight;
+ } else {
+ tooltipY = root.barThickness + root.barSpacing + Theme.spacingXS;
+ }
+
+ tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, currentScreen, false, false);
+ }
+ }
+ onExited: {
+ if (tooltipLoader.item) {
+ tooltipLoader.item.hide();
+ }
+ tooltipLoader.active = false;
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Weather.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Weather.qml
new file mode 100644
index 0000000..5dbe53a
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Weather.qml
@@ -0,0 +1,80 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Plugins
+import qs.Services
+import qs.Widgets
+
+BasePill {
+ id: root
+
+ visible: SettingsData.weatherEnabled
+
+ Ref {
+ service: WeatherService
+ }
+
+ content: Component {
+ Item {
+ implicitWidth: {
+ if (!SettingsData.weatherEnabled) return 0
+ if (root.isVerticalOrientation) return root.widgetThickness - root.horizontalPadding * 2
+ return Math.min(100 - root.horizontalPadding * 2, weatherRow.implicitWidth)
+ }
+ implicitHeight: root.isVerticalOrientation ? weatherColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
+
+ Column {
+ id: weatherColumn
+ visible: root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: 1
+
+ DankIcon {
+ name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
+ size: Theme.barIconSize(root.barThickness, -6, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetIconColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ if (!WeatherService.weather.available) {
+ return "--";
+ }
+ const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
+ return temp;
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Row {
+ id: weatherRow
+ visible: !root.isVerticalOrientation
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
+ size: Theme.barIconSize(root.barThickness, -6, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ color: Theme.widgetIconColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: {
+ if (!WeatherService.weather.available) {
+ return "--°" + (SettingsData.useFahrenheit ? "F" : "C");
+ }
+ const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
+ return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C");
+ }
+ font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
+ color: Theme.widgetTextColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml
new file mode 100644
index 0000000..3721e35
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml
@@ -0,0 +1,1981 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Hyprland
+import Quickshell.I3
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property bool isVertical: axis?.isVertical ?? false
+ property var axis: null
+ property string screenName: ""
+ property real widgetHeight: 30
+ property real barThickness: 48
+ property var barConfig: null
+ property var blurBarWindow: null
+ property var hyprlandOverviewLoader: null
+ property var parentScreen: null
+
+ readonly property real _leftMargin: {
+ if (isVertical)
+ return 0;
+ root.x;
+ if (!root.parent)
+ return 0;
+ const gap = root.mapToItem(null, 0, 0).x;
+ return (gap > 0 && gap < 30) ? gap + 5 : 0;
+ }
+ readonly property real _rightMargin: {
+ if (isVertical)
+ return 0;
+ root.x;
+ root.width;
+ if (!root.parent || !blurBarWindow)
+ return 0;
+ const gap = blurBarWindow.width - root.mapToItem(null, root.width, 0).x;
+ return (gap > 0 && gap < 30) ? gap + 5 : 0;
+ }
+ readonly property real _topMargin: {
+ if (!isVertical)
+ return 0;
+ root.y;
+ if (!root.parent)
+ return 0;
+ const gap = root.mapToItem(null, 0, 0).y;
+ return (gap > 0 && gap < 30) ? gap + 5 : 0;
+ }
+ readonly property real _bottomMargin: {
+ if (!isVertical)
+ return 0;
+ root.y;
+ root.height;
+ if (!root.parent || !blurBarWindow)
+ return 0;
+ const gap = blurBarWindow.height - root.mapToItem(null, 0, root.height).y;
+ return (gap > 0 && gap < 30) ? gap + 5 : 0;
+ }
+
+ property int _desktopEntriesUpdateTrigger: 0
+ readonly property var sortedToplevels: {
+ return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
+ }
+
+ readonly property string effectiveScreenName: {
+ if (!SettingsData.workspaceFollowFocus)
+ return root.screenName;
+
+ switch (CompositorService.compositor) {
+ case "niri":
+ return NiriService.currentOutput || root.screenName;
+ case "hyprland":
+ return Hyprland.focusedWorkspace?.monitor?.name || root.screenName;
+ case "dwl":
+ return DwlService.activeOutput || root.screenName;
+ case "sway":
+ case "scroll":
+ case "miracle":
+ const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
+ return focusedWs?.monitor?.name || root.screenName;
+ default:
+ return root.screenName;
+ }
+ }
+
+ readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle && ExtWorkspaceService.extWorkspaceAvailable)
+
+ Connections {
+ target: DesktopEntries
+ function onApplicationsChanged() {
+ _desktopEntriesUpdateTrigger++;
+ }
+ }
+
+ property var currentWorkspace: {
+ if (useExtWorkspace)
+ return getExtWorkspaceActiveWorkspace();
+
+ switch (CompositorService.compositor) {
+ case "niri":
+ return getNiriActiveWorkspace();
+ case "hyprland":
+ return getHyprlandActiveWorkspace();
+ case "dwl":
+ const activeTags = getDwlActiveTags();
+ return activeTags.length > 0 ? activeTags[0] : -1;
+ case "sway":
+ case "scroll":
+ case "miracle":
+ return getSwayActiveWorkspace();
+ default:
+ return 1;
+ }
+ }
+ property var dwlActiveTags: {
+ if (CompositorService.isDwl) {
+ return getDwlActiveTags();
+ }
+ return [];
+ }
+ property var workspaceList: {
+ if (useExtWorkspace) {
+ const baseList = getExtWorkspaceWorkspaces();
+ return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList;
+ }
+
+ let baseList;
+ switch (CompositorService.compositor) {
+ case "niri":
+ baseList = getNiriWorkspaces();
+ break;
+ case "hyprland":
+ baseList = getHyprlandWorkspaces();
+ break;
+ case "dwl":
+ baseList = getDwlTags();
+ break;
+ case "sway":
+ case "scroll":
+ case "miracle":
+ baseList = getSwayWorkspaces();
+ break;
+ default:
+ return [1];
+ }
+ return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList;
+ }
+
+ function getSwayWorkspaces() {
+ const workspaces = I3.workspaces?.values || [];
+ if (workspaces.length === 0)
+ return [
+ {
+ "num": 1
+ }
+ ];
+
+ function mapWorkspace(ws) {
+ return {
+ "num": ws.number,
+ "name": ws.name,
+ "focused": ws.focused,
+ "active": ws.active,
+ "urgent": ws.urgent,
+ "monitor": ws.monitor
+ };
+ }
+
+ if (!root.screenName || SettingsData.workspaceFollowFocus) {
+ return workspaces.slice().sort((a, b) => a.num - b.num).map(mapWorkspace);
+ }
+
+ const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName);
+ return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num).map(mapWorkspace) : [
+ {
+ "num": 1
+ }
+ ];
+ }
+
+ function getSwayActiveWorkspace() {
+ if (!root.screenName || SettingsData.workspaceFollowFocus) {
+ const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
+ return focusedWs ? focusedWs.num : 1;
+ }
+
+ const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true);
+ return focusedWs ? focusedWs.num : 1;
+ }
+
+ function getHyprlandWorkspaces() {
+ const workspaces = Hyprland.workspaces?.values || [];
+ if (workspaces.length === 0) {
+ return [
+ {
+ id: 1,
+ name: "1"
+ }
+ ];
+ }
+
+ let filtered = workspaces.filter(ws => ws.id > -1);
+ if (filtered.length === 0) {
+ return [
+ {
+ id: 1,
+ name: "1"
+ }
+ ];
+ }
+
+ if (!root.screenName || SettingsData.workspaceFollowFocus) {
+ filtered = filtered.slice().sort((a, b) => a.id - b.id);
+ } else {
+ const monitorWorkspaces = filtered.filter(ws => ws.monitor?.name === root.screenName);
+ filtered = monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.id - b.id) : [
+ {
+ id: 1,
+ name: "1"
+ }
+ ];
+ }
+
+ if (!SettingsData.showOccupiedWorkspacesOnly) {
+ return filtered;
+ }
+
+ const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
+ const activeWsId = root.currentWorkspace;
+ return filtered.filter(ws => {
+ if (ws.id === activeWsId)
+ return true;
+ return hyprlandToplevels.some(tl => tl.workspace?.id === ws.id);
+ });
+ }
+
+ function getHyprlandActiveWorkspace() {
+ if (!root.screenName || SettingsData.workspaceFollowFocus) {
+ return Hyprland.focusedWorkspace?.id || 1;
+ }
+
+ const monitor = Hyprland.monitors?.values?.find(m => m.name === root.screenName);
+ return monitor?.activeWorkspace?.id || 1;
+ }
+
+ function getWorkspaceIcons(ws) {
+ _desktopEntriesUpdateTrigger;
+ if (!SettingsData.showWorkspaceApps || !ws) {
+ return [];
+ }
+
+ let targetWorkspaceId;
+ if (CompositorService.isNiri) {
+ if (!ws || typeof ws !== "object") {
+ const wsNumber = typeof ws === "number" ? ws : -1;
+ if (wsNumber <= 0) {
+ return [];
+ }
+ const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.effectiveScreenName);
+ if (!workspace) {
+ return [];
+ }
+ targetWorkspaceId = workspace.id;
+ } else {
+ if (ws.id === undefined || ws.id === -1 || ws.idx === -1) {
+ return [];
+ }
+ targetWorkspaceId = ws.id;
+ }
+ } else if (CompositorService.isHyprland) {
+ targetWorkspaceId = ws.id !== undefined ? ws.id : ws;
+ } else if (CompositorService.isDwl) {
+ if (typeof ws !== "object" || ws.tag === undefined) {
+ return [];
+ }
+ targetWorkspaceId = ws.tag;
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ targetWorkspaceId = ws.num !== undefined ? ws.num : ws;
+ } else {
+ return [];
+ }
+
+ const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels;
+
+ const byApp = {};
+ let isActiveWs = false;
+ if (CompositorService.isNiri) {
+ isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active);
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
+ isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
+ } else if (CompositorService.isDwl) {
+ const output = DwlService.getOutputState(root.effectiveScreenName);
+ if (output && output.tags) {
+ const tag = output.tags.find(t => t.tag === targetWorkspaceId);
+ isActiveWs = tag ? (tag.state === 1) : false;
+ }
+ } else {
+ isActiveWs = targetWorkspaceId === root.currentWorkspace;
+ }
+
+ wins.forEach((w, i) => {
+ if (!w) {
+ return;
+ }
+
+ let winWs = null;
+ if (CompositorService.isNiri) {
+ winWs = w.workspace_id;
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ winWs = w.workspace?.num;
+ } else {
+ const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
+ const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w);
+ winWs = hyprToplevel?.workspace?.id;
+ }
+
+ if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
+ return;
+ }
+
+ const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown");
+ const moddedId = Paths.moddedAppId(keyBase);
+ const key = isActiveWs || !SettingsData.groupWorkspaceApps ? `${moddedId}_${i}` : moddedId;
+
+ if (!byApp[key]) {
+ const isQuickshell = keyBase === "org.quickshell" || keyBase === "com.danklinux.dms";
+ const isSteamApp = Paths.isSteamApp(moddedId);
+ const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
+ const icon = Paths.getAppIcon(moddedId, desktopEntry);
+ const appName = Paths.getAppName(moddedId, desktopEntry);
+ byApp[key] = {
+ "type": "icon",
+ "icon": icon,
+ "isQuickshell": isQuickshell,
+ "isSteamApp": isSteamApp,
+ "active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
+ "count": 1,
+ "windowId": w.address || w.id,
+ "fallbackText": appName || ""
+ };
+ } else {
+ byApp[key].count++;
+ if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) {
+ byApp[key].active = true;
+ }
+ }
+ });
+
+ return Object.values(byApp);
+ }
+
+ function padWorkspaces(list) {
+ const padded = list.slice();
+ let placeholder;
+ if (useExtWorkspace) {
+ placeholder = {
+ "id": "",
+ "name": "",
+ "active": false,
+ "hidden": true
+ };
+ } else if (CompositorService.isNiri) {
+ placeholder = {
+ "id": -1,
+ "idx": -1,
+ "name": ""
+ };
+ } else if (CompositorService.isHyprland) {
+ placeholder = {
+ "id": -1,
+ "name": ""
+ };
+ } else if (CompositorService.isDwl) {
+ placeholder = {
+ "tag": -1
+ };
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ placeholder = {
+ "num": -1
+ };
+ } else {
+ placeholder = -1;
+ }
+ while (padded.length < 3) {
+ padded.push(placeholder);
+ }
+ return padded;
+ }
+
+ function getNiriWorkspaces() {
+ if (NiriService.allWorkspaces.length === 0) {
+ return [
+ {
+ "id": 1,
+ "idx": 0,
+ "name": ""
+ },
+ {
+ "id": 2,
+ "idx": 1,
+ "name": ""
+ }
+ ];
+ }
+
+ const fallbackWorkspaces = [
+ {
+ "id": 1,
+ "idx": 0,
+ "name": ""
+ },
+ {
+ "id": 2,
+ "idx": 1,
+ "name": ""
+ }
+ ];
+
+ let workspaces;
+ if (!root.screenName || SettingsData.workspaceFollowFocus) {
+ const currentWorkspaces = NiriService.getCurrentOutputWorkspaces();
+ workspaces = currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces;
+ } else {
+ const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName);
+ workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : fallbackWorkspaces;
+ }
+
+ workspaces = workspaces.slice().sort((a, b) => a.idx - b.idx);
+
+ if (!SettingsData.showOccupiedWorkspacesOnly) {
+ return workspaces;
+ }
+
+ return workspaces.filter(ws => {
+ if (ws.is_active)
+ return true;
+ return NiriService.windows?.some(win => win.workspace_id === ws.id) ?? false;
+ });
+ }
+
+ function getNiriActiveWorkspace() {
+ if (NiriService.allWorkspaces.length === 0) {
+ return 1;
+ }
+
+ if (!root.screenName || SettingsData.workspaceFollowFocus) {
+ return NiriService.getCurrentWorkspaceNumber();
+ }
+
+ const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active);
+ return activeWs ? activeWs.idx : 1;
+ }
+
+ function getDwlTags() {
+ if (!DwlService.dwlAvailable)
+ return [];
+
+ const targetScreen = root.effectiveScreenName;
+ const output = DwlService.getOutputState(targetScreen);
+ if (!output || !output.tags || output.tags.length === 0)
+ return [];
+
+ if (SettingsData.dwlShowAllTags) {
+ return output.tags.map(tag => ({
+ "tag": tag.tag,
+ "state": tag.state,
+ "clients": tag.clients,
+ "focused": tag.focused
+ }));
+ }
+
+ const visibleTagIndices = DwlService.getVisibleTags(targetScreen);
+ return visibleTagIndices.map(tagIndex => {
+ const tagData = output.tags.find(t => t.tag === tagIndex);
+ return {
+ "tag": tagIndex,
+ "state": tagData?.state ?? 0,
+ "clients": tagData?.clients ?? 0,
+ "focused": tagData?.focused ?? false
+ };
+ });
+ }
+
+ function getDwlActiveTags() {
+ if (!DwlService.dwlAvailable)
+ return [];
+
+ return DwlService.getActiveTags(root.effectiveScreenName);
+ }
+
+ function getExtWorkspaceWorkspaces() {
+ const groups = ExtWorkspaceService.groups;
+ if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) {
+ return [
+ {
+ "id": "1",
+ "name": "1",
+ "active": false
+ }
+ ];
+ }
+
+ const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName));
+ if (!group || !group.workspaces) {
+ return [
+ {
+ "id": "1",
+ "name": "1",
+ "active": false
+ }
+ ];
+ }
+
+ let visible = group.workspaces.filter(ws => !ws.hidden);
+
+ const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0);
+ if (hasValidCoordinates) {
+ visible = visible.sort((a, b) => {
+ const coordsA = a.coordinates || [0, 0];
+ const coordsB = b.coordinates || [0, 0];
+ if (coordsA[0] !== coordsB[0])
+ return coordsA[0] - coordsB[0];
+ return coordsA[1] - coordsB[1];
+ });
+ }
+
+ visible = visible.map(ws => ({
+ id: ws.id,
+ name: ws.name,
+ coordinates: ws.coordinates,
+ state: ws.state,
+ active: ws.active,
+ urgent: ws.urgent,
+ hidden: ws.hidden,
+ groupID: group.id
+ }));
+
+ return visible.length > 0 ? visible : [
+ {
+ "id": "1",
+ "name": "1",
+ "active": false
+ }
+ ];
+ }
+
+ function getExtWorkspaceActiveWorkspace() {
+ if (!ExtWorkspaceService.extWorkspaceAvailable) {
+ return 1;
+ }
+
+ const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName);
+ return activeWs ? (activeWs.id || activeWs.name || "1") : "1";
+ }
+
+ readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1
+ readonly property real padding: (root.barConfig?.removeWidgetPadding ?? false) ? 0 : Theme.snap((root.barConfig?.widgetPadding ?? 12) * (widgetHeight / 30), dpr)
+ readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
+ readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
+ readonly property real appIconSize: Theme.barIconSize(barThickness, -6 + SettingsData.workspaceAppIconSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+
+ function getRealWorkspaces() {
+ return root.workspaceList.filter(ws => {
+ if (useExtWorkspace)
+ return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
+ if (CompositorService.isNiri)
+ return ws && ws.idx !== -1;
+ if (CompositorService.isHyprland)
+ return ws && ws.id !== -1;
+ if (CompositorService.isDwl)
+ return ws && ws.tag !== -1;
+ if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
+ return ws && ws.num !== -1;
+ return ws !== -1;
+ });
+ }
+
+ function switchToWorkspaceByModelData(data) {
+ if (!data)
+ return;
+
+ if (root.useExtWorkspace && (data.id || data.name)) {
+ ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || "");
+ return;
+ }
+
+ switch (CompositorService.compositor) {
+ case "niri":
+ if (data.idx !== undefined)
+ NiriService.switchToWorkspace(data.idx);
+ break;
+ case "hyprland":
+ if (data.id)
+ Hyprland.dispatch(`workspace ${data.id}`);
+ break;
+ case "dwl":
+ if (data.tag !== undefined)
+ DwlService.switchToTag(root.screenName, data.tag);
+ break;
+ case "sway":
+ case "scroll":
+ case "miracle":
+ if (data.num)
+ try {
+ I3.dispatch(`workspace number ${data.num}`);
+ } catch (_) {}
+ break;
+ }
+ }
+
+ function findClosestWorkspaceIndex(localX, localY) {
+ if (workspaceRepeater.count === 0)
+ return -1;
+
+ let closestIdx = -1;
+ let closestDist = Infinity;
+
+ for (let i = 0; i < workspaceRepeater.count; i++) {
+ const item = workspaceRepeater.itemAt(i);
+ if (!item)
+ continue;
+ const center = item.mapToItem(root, item.width / 2, item.height / 2);
+ const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x);
+ if (dist < closestDist) {
+ closestDist = dist;
+ closestIdx = i;
+ }
+ }
+ return closestIdx;
+ }
+
+ function switchWorkspace(direction) {
+ if (useExtWorkspace) {
+ const realWorkspaces = getRealWorkspaces();
+ if (realWorkspaces.length < 2) {
+ return;
+ }
+
+ const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace);
+ const validIndex = currentIndex === -1 ? 0 : currentIndex;
+ const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
+
+ if (nextIndex === validIndex) {
+ return;
+ }
+
+ const nextWorkspace = realWorkspaces[nextIndex];
+ ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || "");
+ } else if (CompositorService.isNiri) {
+ const realWorkspaces = getRealWorkspaces();
+ if (realWorkspaces.length < 2) {
+ return;
+ }
+
+ const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === root.currentWorkspace);
+ const validIndex = currentIndex === -1 ? 0 : currentIndex;
+ const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
+
+ if (nextIndex === validIndex) {
+ return;
+ }
+
+ const nextWorkspace = realWorkspaces[nextIndex];
+ if (!nextWorkspace || nextWorkspace.idx === undefined) {
+ return;
+ }
+ NiriService.switchToWorkspace(nextWorkspace.idx);
+ } else if (CompositorService.isHyprland) {
+ const realWorkspaces = getRealWorkspaces();
+ if (realWorkspaces.length < 2) {
+ return;
+ }
+
+ const currentIndex = realWorkspaces.findIndex(ws => ws.id === root.currentWorkspace);
+ const validIndex = currentIndex === -1 ? 0 : currentIndex;
+ const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
+
+ if (nextIndex === validIndex) {
+ return;
+ }
+
+ Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`);
+ } else if (CompositorService.isDwl) {
+ const realWorkspaces = getRealWorkspaces();
+ if (realWorkspaces.length < 2) {
+ return;
+ }
+
+ const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace);
+ const validIndex = currentIndex === -1 ? 0 : currentIndex;
+ const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
+
+ if (nextIndex === validIndex) {
+ return;
+ }
+
+ DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ const realWorkspaces = getRealWorkspaces();
+ if (realWorkspaces.length < 2) {
+ return;
+ }
+
+ const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace);
+ const validIndex = currentIndex === -1 ? 0 : currentIndex;
+ const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
+
+ if (nextIndex === validIndex) {
+ return;
+ }
+
+ try {
+ I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`);
+ } catch (_) {}
+ }
+ }
+
+ function getWorkspaceIndexFallback(modelData, index) {
+ if (root.useExtWorkspace)
+ return index + 1;
+ if (CompositorService.isNiri)
+ return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : "";
+ if (CompositorService.isHyprland)
+ return modelData?.id || "";
+ if (CompositorService.isDwl)
+ return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
+ if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
+ return modelData?.num || "";
+ return modelData - 1;
+ }
+
+ function getWorkspaceIndex(modelData, index) {
+ let isPlaceholder;
+ if (root.useExtWorkspace) {
+ isPlaceholder = modelData?.hidden === true;
+ } else if (CompositorService.isNiri) {
+ isPlaceholder = modelData?.idx === -1;
+ } else if (CompositorService.isHyprland) {
+ isPlaceholder = modelData?.id === -1;
+ } else if (CompositorService.isDwl) {
+ isPlaceholder = modelData?.tag === -1;
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ isPlaceholder = modelData?.num === -1;
+ } else {
+ isPlaceholder = modelData === -1;
+ }
+
+ if (isPlaceholder)
+ return index + 1;
+
+ let workspaceName = "";
+ if (SettingsData.showWorkspaceName) {
+ workspaceName = modelData?.name ?? "";
+
+ if (workspaceName && workspaceName !== "") {
+ if (root.isVertical) {
+ workspaceName = workspaceName.charAt(0);
+ }
+ } else {
+ workspaceName = "";
+ }
+ }
+
+ if (workspaceName) {
+ if (SettingsData.showWorkspaceIndex) {
+ const indexLabel = getWorkspaceIndexFallback(modelData, index);
+ return indexLabel ? `${indexLabel}: ${workspaceName}` : workspaceName;
+ }
+ return workspaceName;
+ }
+
+ return getWorkspaceIndexFallback(modelData, index);
+ }
+
+ readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
+ readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
+ readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
+
+ width: shouldShow ? (isVertical ? barThickness : visualWidth) : 0
+ height: shouldShow ? (isVertical ? visualHeight : barThickness) : 0
+ visible: shouldShow
+
+ Item {
+ id: visualBackground
+ width: root.visualWidth
+ height: root.visualHeight
+ anchors.centerIn: parent
+
+ Rectangle {
+ id: outline
+ anchors.centerIn: parent
+ width: {
+ const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
+ return parent.width + borderWidth * 2;
+ }
+ height: {
+ const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
+ return parent.height + borderWidth * 2;
+ }
+ radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
+ color: "transparent"
+ border.width: {
+ if (barConfig?.widgetOutlineEnabled ?? false) {
+ return barConfig?.widgetOutlineThickness ?? 1;
+ }
+ return 0;
+ }
+ border.color: {
+ if (!(barConfig?.widgetOutlineEnabled ?? false)) {
+ return "transparent";
+ }
+ const colorOption = barConfig?.widgetOutlineColor || "primary";
+ const opacity = barConfig?.widgetOutlineOpacity ?? 1.0;
+ switch (colorOption) {
+ case "surfaceText":
+ return Theme.withAlpha(Theme.surfaceText, opacity);
+ case "secondary":
+ return Theme.withAlpha(Theme.secondary, opacity);
+ case "primary":
+ return Theme.withAlpha(Theme.primary, opacity);
+ default:
+ return Theme.withAlpha(Theme.primary, opacity);
+ }
+ }
+ }
+
+ Rectangle {
+ id: background
+ anchors.fill: parent
+ radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
+ color: {
+ if ((barConfig?.noBackground ?? false))
+ return "transparent";
+ const baseColor = Theme.widgetBaseBackgroundColor;
+ const transparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
+ if (Theme.widgetBackgroundHasAlpha) {
+ return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
+ }
+ return Theme.withAlpha(baseColor, transparency);
+ }
+ }
+ }
+
+ MouseArea {
+ id: edgeMouseArea
+ z: -1
+ x: -root._leftMargin
+ y: -root._topMargin
+ width: root.width + root._leftMargin + root._rightMargin
+ height: root.height + root._topMargin + root._bottomMargin
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+
+ property real touchpadAccumulator: 0
+ property real mouseAccumulator: 0
+ property bool scrollInProgress: false
+
+ Timer {
+ id: scrollCooldown
+ interval: 100
+ onTriggered: parent.scrollInProgress = false
+ }
+
+ onClicked: mouse => {
+ const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
+ switch (mouse.button) {
+ case Qt.RightButton:
+ if (CompositorService.isNiri) {
+ NiriService.toggleOverview();
+ } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
+ root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
+ }
+ break;
+ case Qt.LeftButton:
+ const idx = root.findClosestWorkspaceIndex(rootPos.x, rootPos.y);
+ if (idx >= 0)
+ root.switchToWorkspaceByModelData(root.workspaceList[idx]);
+ break;
+ }
+ }
+
+ onWheel: wheel => {
+ if (Math.abs(wheel.angleDelta.x) > Math.abs(wheel.angleDelta.y)) {
+ wheel.accepted = false;
+ return;
+ }
+
+ if (scrollInProgress)
+ return;
+
+ const delta = wheel.angleDelta.y;
+ const isTouchpad = wheel.pixelDelta && wheel.pixelDelta.y !== 0;
+ const reverse = SettingsData.reverseScrolling ? -1 : 1;
+
+ if (isTouchpad) {
+ touchpadAccumulator += delta;
+ if (Math.abs(touchpadAccumulator) < 500)
+ return;
+ const direction = touchpadAccumulator * reverse < 0 ? 1 : -1;
+ root.switchWorkspace(direction);
+ scrollInProgress = true;
+ scrollCooldown.restart();
+ touchpadAccumulator = 0;
+ return;
+ }
+
+ mouseAccumulator += delta;
+ if (Math.abs(mouseAccumulator) < 120)
+ return;
+ const direction = mouseAccumulator * reverse < 0 ? 1 : -1;
+ root.switchWorkspace(direction);
+ scrollInProgress = true;
+ scrollCooldown.restart();
+ mouseAccumulator = 0;
+ }
+ }
+
+ property int dragSourceIndex: -1
+ property int dragTargetIndex: -1
+ property bool suppressShiftAnimation: false
+
+ onWorkspaceListChanged: {
+ if (dragSourceIndex >= 0) {
+ dragSourceIndex = -1;
+ dragTargetIndex = -1;
+ suppressShiftAnimation = false;
+ }
+ }
+
+ Flow {
+ id: workspaceRow
+
+ x: isVertical ? visualBackground.x : (parent.width - implicitWidth) / 2
+ y: isVertical ? (parent.height - implicitHeight) / 2 : visualBackground.y
+ spacing: Theme.spacingS
+ flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
+
+ Repeater {
+ id: workspaceRepeater
+ model: ScriptModel {
+ values: root.workspaceList
+ }
+
+ Item {
+ id: delegateRoot
+
+ property bool isDropTarget: root.dragTargetIndex === index
+
+ z: dragHandler.dragging ? 1000 : 1
+
+ property real shiftOffset: {
+ if (root.dragSourceIndex < 0 || index === root.dragSourceIndex)
+ return 0;
+ const dragIdx = root.dragSourceIndex;
+ const dropIdx = root.dragTargetIndex;
+ if (dropIdx < 0)
+ return 0;
+ const shiftAmount = delegateRoot.width + Theme.spacingS;
+ if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
+ return -shiftAmount;
+ if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
+ return shiftAmount;
+ return 0;
+ }
+
+ transform: Translate {
+ x: root.isVertical ? 0 : delegateRoot.shiftOffset
+ y: root.isVertical ? delegateRoot.shiftOffset : 0
+ Behavior on x {
+ enabled: !root.suppressShiftAnimation
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutCubic
+ }
+ }
+ Behavior on y {
+ enabled: !root.suppressShiftAnimation
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ property bool isActive: {
+ if (root.useExtWorkspace)
+ return (modelData?.id || modelData?.name) === root.currentWorkspace;
+ if (CompositorService.isNiri)
+ return !!(modelData && modelData.idx === root.currentWorkspace);
+ if (CompositorService.isHyprland)
+ return !!(modelData && modelData.id === root.currentWorkspace);
+ if (CompositorService.isDwl)
+ return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
+ if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
+ return !!(modelData && modelData.num === root.currentWorkspace);
+ return modelData === root.currentWorkspace;
+ }
+ property bool isOccupied: {
+ if (CompositorService.isHyprland)
+ return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
+ if (CompositorService.isDwl)
+ return modelData.clients > 0;
+ if (CompositorService.isNiri) {
+ const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName);
+ return workspace ? (NiriService.windows?.some(win => win.workspace_id === workspace.id) ?? false) : false;
+ }
+ return false;
+ }
+ property bool isPlaceholder: {
+ if (root.useExtWorkspace)
+ return !!(modelData && modelData.hidden);
+ if (CompositorService.isNiri)
+ return !!(modelData && modelData.idx === -1);
+ if (CompositorService.isHyprland)
+ return !!(modelData && modelData.id === -1);
+ if (CompositorService.isDwl)
+ return !!(modelData && modelData.tag === -1);
+ if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
+ return !!(modelData && modelData.num === -1);
+ return modelData === -1;
+ }
+ property bool isHovered: mouseArea.containsMouse
+
+ property var loadedWorkspaceData: null
+ property bool loadedIsUrgent: false
+ property bool isUrgent: {
+ if (root.useExtWorkspace)
+ return modelData?.urgent ?? false;
+ if (CompositorService.isHyprland)
+ return modelData?.urgent ?? false;
+ if (CompositorService.isNiri)
+ return loadedIsUrgent;
+ if (CompositorService.isDwl)
+ return modelData?.state === 2;
+ if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
+ return loadedIsUrgent;
+ return false;
+ }
+ readonly property var loadedIconData: {
+ if (isPlaceholder)
+ return null;
+ const name = modelData?.name;
+ if (!name)
+ return null;
+ return SettingsData.getWorkspaceNameIcon(name);
+ }
+ readonly property bool loadedHasIcon: loadedIconData !== null
+ property var loadedIcons: []
+
+ readonly property int stableIconCount: {
+ if (!SettingsData.showWorkspaceApps || isPlaceholder)
+ return 0;
+
+ let targetWorkspaceId;
+ if (root.useExtWorkspace) {
+ targetWorkspaceId = modelData?.id || modelData?.name;
+ } else if (CompositorService.isNiri) {
+ targetWorkspaceId = modelData?.id;
+ } else if (CompositorService.isHyprland) {
+ targetWorkspaceId = modelData?.id;
+ } else if (CompositorService.isDwl) {
+ targetWorkspaceId = modelData?.tag;
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ targetWorkspaceId = modelData?.num;
+ }
+ if (targetWorkspaceId === undefined || targetWorkspaceId === null)
+ return 0;
+
+ const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels;
+ const seen = {};
+ let groupedCount = 0;
+ let totalCount = 0;
+
+ for (let i = 0; i < wins.length; i++) {
+ const w = wins[i];
+ if (!w)
+ continue;
+
+ let winWs = null;
+ if (CompositorService.isNiri) {
+ winWs = w.workspace_id;
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ winWs = w.workspace?.num;
+ } else if (CompositorService.isHyprland) {
+ const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
+ const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w);
+ winWs = hyprToplevel?.workspace?.id;
+ }
+
+ if (winWs !== targetWorkspaceId)
+ continue;
+ totalCount++;
+
+ const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown";
+ if (!seen[appKey]) {
+ seen[appKey] = true;
+ groupedCount++;
+ }
+ }
+
+ return (SettingsData.groupWorkspaceApps && !isActive) ? groupedCount : totalCount;
+ }
+
+ readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2))
+ readonly property real baseHeight: root.isVertical ? (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5)
+ readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
+ readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
+ readonly property real contentImplicitWidth: appIconsLoader.item?.contentWidth ?? 0
+ readonly property real contentImplicitHeight: appIconsLoader.item?.contentHeight ?? 0
+
+ readonly property real iconsExtraWidth: {
+ if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
+ const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
+ return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
+ }
+ return 0;
+ }
+ readonly property real iconsExtraHeight: {
+ if (root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
+ const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
+ return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
+ }
+ return 0;
+ }
+
+ readonly property real visualWidth: {
+ if (contentImplicitWidth <= 0)
+ return baseWidth + iconsExtraWidth;
+ const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS;
+ return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding);
+ }
+ readonly property real visualHeight: {
+ if (contentImplicitHeight <= 0)
+ return baseHeight + iconsExtraHeight;
+ const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS;
+ return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding);
+ }
+
+ readonly property color unfocusedColor: {
+ switch (SettingsData.workspaceUnfocusedColorMode) {
+ case "s":
+ return Theme.surface;
+ case "sc":
+ return Theme.surfaceContainer;
+ case "sch":
+ return Theme.surfaceContainerHigh;
+ default:
+ return Theme.surfaceTextAlpha;
+ }
+ }
+
+ readonly property color activeColor: {
+ switch (SettingsData.workspaceColorMode) {
+ case "s":
+ return Theme.surface;
+ case "sc":
+ return Theme.surfaceContainer;
+ case "sch":
+ return Theme.surfaceContainerHigh;
+ case "none":
+ return unfocusedColor;
+ default:
+ return Theme.primary;
+ }
+ }
+
+ readonly property color occupiedColor: {
+ switch (SettingsData.workspaceOccupiedColorMode) {
+ case "sec":
+ return Theme.secondary;
+ case "s":
+ return Theme.surface;
+ case "sc":
+ return Theme.surfaceContainer;
+ case "sch":
+ return Theme.surfaceContainerHigh;
+ case "schh":
+ return Theme.surfaceContainerHighest;
+ default:
+ return unfocusedColor;
+ }
+ }
+
+ readonly property color urgentColor: {
+ switch (SettingsData.workspaceUrgentColorMode) {
+ case "primary":
+ return Theme.primary;
+ case "secondary":
+ return Theme.secondary;
+ case "s":
+ return Theme.surface;
+ case "sc":
+ return Theme.surfaceContainer;
+ default:
+ return Theme.error;
+ }
+ }
+
+ readonly property color focusedBorderColor: {
+ switch (SettingsData.workspaceFocusedBorderColor) {
+ case "surfaceText":
+ return Theme.surfaceText;
+ case "secondary":
+ return Theme.secondary;
+ default:
+ return Theme.primary;
+ }
+ }
+
+ function getContrastingIconColor(bgColor) {
+ const luminance = 0.299 * bgColor.r + 0.587 * bgColor.g + 0.114 * bgColor.b;
+ return luminance > 0.4 ? Qt.rgba(0.15, 0.15, 0.15, 1) : Qt.rgba(0.8, 0.8, 0.8, 1);
+ }
+
+ readonly property color quickshellIconActiveColor: getContrastingIconColor(activeColor)
+ readonly property color quickshellIconInactiveColor: getContrastingIconColor(unfocusedColor)
+
+ Item {
+ id: dragHandler
+ anchors.fill: parent
+ property bool dragging: false
+ property point dragStartPos: Qt.point(0, 0)
+ property real dragAxisOffset: 0
+
+ Connections {
+ target: root
+ function onWorkspaceListChanged() {
+ if (dragHandler.dragging) {
+ dragHandler.dragging = false;
+ dragHandler.dragAxisOffset = 0;
+ mouseArea.mousePressed = false;
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: !isPlaceholder
+ cursorShape: isPlaceholder ? Qt.ArrowCursor : (dragHandler.dragging ? Qt.ClosedHandCursor : Qt.PointingHandCursor)
+ enabled: !isPlaceholder
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ property bool mousePressed: false
+
+ onPressed: mouse => {
+ if (mouse.button === Qt.LeftButton && CompositorService.isNiri && SettingsData.workspaceDragReorder && !isPlaceholder) {
+ mousePressed = true;
+ dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
+ }
+ }
+
+ onPositionChanged: mouse => {
+ if (!mousePressed || !CompositorService.isNiri || !SettingsData.workspaceDragReorder || isPlaceholder)
+ return;
+
+ if (!dragHandler.dragging) {
+ const distance = root.isVertical ? Math.abs(mouse.y - dragHandler.dragStartPos.y) : Math.abs(mouse.x - dragHandler.dragStartPos.x);
+ if (distance > 5) {
+ dragHandler.dragging = true;
+ root.dragSourceIndex = index;
+ root.dragTargetIndex = index;
+ }
+ }
+
+ if (!dragHandler.dragging)
+ return;
+
+ const rawAxisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
+
+ const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS;
+ const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize;
+ const maxOffsetNegative = -index * itemSize;
+ const axisOffset = Math.max(maxOffsetNegative, Math.min(maxOffsetPositive, rawAxisOffset));
+ dragHandler.dragAxisOffset = axisOffset;
+
+ const slotOffset = Math.round(axisOffset / itemSize);
+ const newTargetIndex = Math.max(0, Math.min(root.workspaceList.length - 1, index + slotOffset));
+
+ if (newTargetIndex !== root.dragTargetIndex) {
+ root.dragTargetIndex = newTargetIndex;
+ }
+ }
+
+ onReleased: mouse => {
+ const wasDragging = dragHandler.dragging;
+ const didReorder = wasDragging && root.dragTargetIndex >= 0 && root.dragTargetIndex !== root.dragSourceIndex;
+
+ if (didReorder) {
+ const sourceWs = root.workspaceList[root.dragSourceIndex];
+ const targetWs = root.workspaceList[root.dragTargetIndex];
+
+ if (sourceWs && targetWs && sourceWs.idx !== undefined && targetWs.idx !== undefined) {
+ root.suppressShiftAnimation = true;
+ NiriService.moveWorkspaceToIndex(sourceWs.idx, targetWs.idx);
+ Qt.callLater(() => root.suppressShiftAnimation = false);
+ }
+ }
+
+ mousePressed = false;
+ dragHandler.dragging = false;
+ dragHandler.dragAxisOffset = 0;
+ root.dragSourceIndex = -1;
+ root.dragTargetIndex = -1;
+
+ if (wasDragging || isPlaceholder)
+ return;
+
+ if (mouse.button === Qt.LeftButton) {
+ if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
+ ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "");
+ } else if (CompositorService.isNiri) {
+ if (modelData && modelData.idx !== undefined) {
+ NiriService.switchToWorkspace(modelData.idx);
+ }
+ } else if (CompositorService.isHyprland && modelData?.id) {
+ Hyprland.dispatch(`workspace ${modelData.id}`);
+ } else if (CompositorService.isDwl && modelData?.tag !== undefined) {
+ DwlService.switchToTag(root.screenName, modelData.tag);
+ } else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
+ try {
+ I3.dispatch(`workspace number ${modelData.num}`);
+ } catch (_) {}
+ }
+ } else if (mouse.button === Qt.RightButton) {
+ if (CompositorService.isNiri) {
+ NiriService.toggleOverview();
+ } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
+ root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
+ } else if (CompositorService.isDwl && modelData?.tag !== undefined) {
+ DwlService.toggleTag(root.screenName, modelData.tag);
+ }
+ }
+ }
+ }
+
+ Timer {
+ id: dataUpdateTimer
+ interval: 50
+ onTriggered: {
+ if (isPlaceholder) {
+ delegateRoot.loadedWorkspaceData = null;
+ delegateRoot.loadedIcons = [];
+ delegateRoot.loadedIsUrgent = false;
+ return;
+ }
+
+ var wsData = null;
+ if (root.useExtWorkspace) {
+ wsData = modelData;
+ } else if (CompositorService.isNiri) {
+ wsData = modelData || null;
+ } else if (CompositorService.isHyprland) {
+ wsData = modelData;
+ } else if (CompositorService.isDwl) {
+ wsData = modelData;
+ } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ wsData = modelData;
+ }
+ delegateRoot.loadedWorkspaceData = wsData;
+ if (CompositorService.isNiri) {
+ const workspaceId = wsData?.id;
+ delegateRoot.loadedIsUrgent = workspaceId ? NiriService.windows.some(w => w.workspace_id === workspaceId && w.is_urgent) : false;
+ } else {
+ delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
+ }
+
+ if (SettingsData.showWorkspaceApps) {
+ if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
+ delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
+ } else if (CompositorService.isNiri) {
+ delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
+ } else {
+ delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
+ }
+ } else {
+ delegateRoot.loadedIcons = [];
+ }
+ }
+ }
+
+ function updateAllData() {
+ dataUpdateTimer.restart();
+ }
+
+ width: root.isVertical ? root.widgetHeight : visualWidth
+ height: root.isVertical ? visualHeight : root.widgetHeight
+
+ Behavior on width {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Rectangle {
+ id: focusedBorderRing
+ x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
+ y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2
+ width: {
+ const borderWidth = (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0;
+ return delegateRoot.visualWidth + borderWidth * 2;
+ }
+ height: {
+ const borderWidth = (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0;
+ return delegateRoot.visualHeight + borderWidth * 2;
+ }
+ radius: Theme.cornerRadius
+ color: "transparent"
+ border.width: (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0
+ border.color: (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? focusedBorderColor : "transparent"
+
+ Behavior on width {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on border.width {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+ }
+
+ Rectangle {
+ id: visualContent
+ width: delegateRoot.visualWidth
+ height: delegateRoot.visualHeight
+ x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
+ y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2
+ radius: Theme.cornerRadius
+ color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : isOccupied ? occupiedColor : unfocusedColor
+ opacity: dragHandler.dragging ? 0.8 : 1.0
+
+ border.width: dragHandler.dragging ? 2 : (isUrgent ? 2 : (isDropTarget ? 2 : 0))
+ border.color: dragHandler.dragging ? Theme.primary : (isUrgent ? urgentColor : (isDropTarget ? Theme.primary : "transparent"))
+
+ transform: Translate {
+ x: root.isVertical ? 0 : (dragHandler.dragging ? dragHandler.dragAxisOffset : 0)
+ y: root.isVertical ? (dragHandler.dragging ? dragHandler.dragAxisOffset : 0) : 0
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on width {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on border.width {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Loader {
+ id: appIconsLoader
+ anchors.fill: parent
+ active: SettingsData.showWorkspaceApps || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon
+ sourceComponent: Item {
+ id: contentRoot
+ readonly property real contentWidth: contentRow.item?.implicitWidth ?? 0
+ readonly property real contentHeight: contentRow.item?.implicitHeight ?? 0
+
+ Loader {
+ id: contentRow
+ anchors.centerIn: parent
+ sourceComponent: root.isVertical ? columnLayout : rowLayout
+ }
+
+ Component {
+ id: rowLayout
+ Row {
+ spacing: 4
+ visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon
+
+ Item {
+ visible: loadedHasIcon && loadedIconData?.type === "icon"
+ width: wsIcon.width
+ height: root.appIconSize
+
+ DankIcon {
+ id: wsIcon
+ anchors.verticalCenter: parent.verticalCenter
+ name: loadedIconData?.value ?? ""
+ size: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
+ weight: (isActive && !isPlaceholder) ? 500 : 400
+ }
+ }
+
+ Item {
+ visible: loadedHasIcon && loadedIconData?.type === "text"
+ width: wsText.implicitWidth
+ height: root.appIconSize
+
+ StyledText {
+ id: wsText
+ anchors.verticalCenter: parent.verticalCenter
+ text: loadedIconData?.value ?? ""
+ color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
+ font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
+ }
+ }
+
+ Item {
+ visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName)
+ width: wsIndexText.implicitWidth
+ height: root.appIconSize
+
+ StyledText {
+ id: wsIndexText
+ anchors.verticalCenter: parent.verticalCenter
+ text: loadedHasIcon ? (modelData?.name ?? "") : root.getWorkspaceIndex(modelData, index)
+ color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
+ font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
+ }
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
+ }
+ delegate: Item {
+ width: root.appIconSize
+ height: root.appIconSize
+ readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
+ readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
+ readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
+ readonly property real appOpacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
+
+ IconImage {
+ id: rowAppIcon
+ anchors.fill: parent
+ source: modelData.icon || ""
+ opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
+ visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ visible: !modelData.isQuickshell && !modelData.isSteamApp && rowAppIcon.status !== Image.Ready
+ color: Theme.surfaceContainer
+ radius: Theme.cornerRadius * (root.appIconSize / 40)
+ border.width: 1
+ border.color: appBorderColor
+ opacity: appOpacity
+
+ StyledText {
+ anchors.centerIn: parent
+ text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
+ font.pixelSize: parent.width * 0.45
+ color: appGlyphColor
+ font.weight: Font.Bold
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ visible: !modelData.isQuickshell && modelData.isSteamApp && rowSteamIcon.status !== Image.Ready
+ color: Theme.surfaceContainer
+ radius: Theme.cornerRadius * (root.appIconSize / 40)
+ border.width: 1
+ border.color: appBorderColor
+ opacity: appOpacity
+
+ DankIcon {
+ anchors.centerIn: parent
+ size: parent.width * 0.7
+ name: "sports_esports"
+ color: appGlyphColor
+ }
+ }
+
+ IconImage {
+ anchors.fill: parent
+ source: modelData.icon
+ opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
+ visible: modelData.isQuickshell
+ layer.enabled: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
+ }
+ }
+
+ IconImage {
+ id: rowSteamIcon
+ anchors.fill: parent
+ source: modelData.icon
+ opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
+ visible: modelData.isSteamApp && modelData.icon
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ size: root.appIconSize
+ name: "sports_esports"
+ color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
+ opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
+ visible: modelData.isSteamApp && !modelData.icon
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
+ color: "transparent"
+ radius: Theme.cornerRadius * (root.appIconSize / 40)
+ border.width: 1
+ border.color: focusedBorderColor
+ z: 1
+ }
+
+ MouseArea {
+ id: rowAppMouseArea
+ anchors.fill: parent
+ enabled: isActive
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ const winId = modelData.windowId;
+ if (!winId)
+ return;
+ if (CompositorService.isHyprland) {
+ Hyprland.dispatch(`focuswindow address:${winId}`);
+ } else if (CompositorService.isNiri) {
+ NiriService.focusWindow(winId);
+ }
+ }
+ }
+
+ Rectangle {
+ visible: modelData.count > 1 && !isActive
+ width: root.appIconSize * 0.67
+ height: root.appIconSize * 0.67
+ radius: root.appIconSize * 0.33
+ color: "black"
+ border.color: "white"
+ border.width: 1
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ z: 2
+
+ Text {
+ anchors.centerIn: parent
+ text: modelData.count
+ font.pixelSize: root.appIconSize * 0.44
+ color: "white"
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: columnLayout
+ Column {
+ spacing: 4
+ visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon
+
+ DankIcon {
+ visible: loadedHasIcon && loadedIconData?.type === "icon"
+ anchors.horizontalCenter: parent.horizontalCenter
+ name: loadedIconData?.value ?? ""
+ size: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
+ weight: (isActive && !isPlaceholder) ? 500 : 400
+ }
+
+ StyledText {
+ visible: loadedHasIcon && loadedIconData?.type === "text"
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: loadedIconData?.value ?? ""
+ color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
+ font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
+ }
+
+ StyledText {
+ visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName)
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: loadedHasIcon ? (root.isVertical ? (modelData?.name ?? "").charAt(0) : (modelData?.name ?? "")) : root.getWorkspaceIndex(modelData, index)
+ color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
+ font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
+ font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
+ }
+ delegate: Item {
+ width: root.appIconSize
+ height: root.appIconSize
+ readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
+ readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
+ readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
+ readonly property real appOpacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
+
+ IconImage {
+ id: colAppIcon
+ anchors.fill: parent
+ source: modelData.icon || ""
+ opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
+ visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ visible: !modelData.isQuickshell && !modelData.isSteamApp && colAppIcon.status !== Image.Ready
+ color: Theme.surfaceContainer
+ radius: Theme.cornerRadius * (root.appIconSize / 40)
+ border.width: 1
+ border.color: appBorderColor
+ opacity: appOpacity
+
+ StyledText {
+ anchors.centerIn: parent
+ text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
+ font.pixelSize: parent.width * 0.45
+ color: appGlyphColor
+ font.weight: Font.Bold
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ visible: !modelData.isQuickshell && modelData.isSteamApp && colSteamIcon.status !== Image.Ready
+ color: Theme.surfaceContainer
+ radius: Theme.cornerRadius * (root.appIconSize / 40)
+ border.width: 1
+ border.color: appBorderColor
+ opacity: appOpacity
+
+ DankIcon {
+ anchors.centerIn: parent
+ size: parent.width * 0.7
+ name: "sports_esports"
+ color: appGlyphColor
+ }
+ }
+
+ IconImage {
+ anchors.fill: parent
+ source: modelData.icon
+ opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
+ visible: modelData.isQuickshell
+ layer.enabled: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
+ }
+ }
+
+ IconImage {
+ id: colSteamIcon
+ anchors.fill: parent
+ source: modelData.icon
+ opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
+ visible: modelData.isSteamApp && modelData.icon
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ size: root.appIconSize
+ name: "sports_esports"
+ color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
+ opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
+ visible: modelData.isSteamApp && !modelData.icon
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
+ color: "transparent"
+ radius: Theme.cornerRadius * (root.appIconSize / 40)
+ border.width: 1
+ border.color: focusedBorderColor
+ z: 1
+ }
+
+ MouseArea {
+ id: colAppMouseArea
+ anchors.fill: parent
+ enabled: isActive
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ const winId = modelData.windowId;
+ if (!winId)
+ return;
+ if (CompositorService.isHyprland) {
+ Hyprland.dispatch(`focuswindow address:${winId}`);
+ } else if (CompositorService.isNiri) {
+ NiriService.focusWindow(winId);
+ }
+ }
+ }
+
+ Rectangle {
+ visible: modelData.count > 1 && !isActive
+ width: root.appIconSize * 0.67
+ height: root.appIconSize * 0.67
+ radius: root.appIconSize * 0.33
+ color: "black"
+ border.color: "white"
+ border.width: 1
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ z: 2
+
+ Text {
+ anchors.centerIn: parent
+ text: modelData.count
+ font.pixelSize: root.appIconSize * 0.44
+ color: "white"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Component.onCompleted: updateAllData()
+
+ Connections {
+ target: CompositorService
+ function onSortedToplevelsChanged() {
+ delegateRoot.updateAllData();
+ }
+ }
+ Connections {
+ target: NiriService
+ enabled: CompositorService.isNiri
+ function onAllWorkspacesChanged() {
+ delegateRoot.updateAllData();
+ }
+ function onWindowUrgentChanged() {
+ delegateRoot.updateAllData();
+ }
+ function onWindowsChanged() {
+ delegateRoot.updateAllData();
+ }
+ }
+ Connections {
+ target: SettingsData
+ function onShowWorkspaceAppsChanged() {
+ delegateRoot.updateAllData();
+ }
+ function onWorkspaceNameIconsChanged() {
+ delegateRoot.updateAllData();
+ }
+ function onAppIdSubstitutionsChanged() {
+ delegateRoot.updateAllData();
+ }
+ }
+ Connections {
+ target: DwlService
+ enabled: CompositorService.isDwl
+ function onStateChanged() {
+ delegateRoot.updateAllData();
+ }
+ }
+ Connections {
+ target: Hyprland.workspaces
+ enabled: CompositorService.isHyprland
+ function onValuesChanged() {
+ delegateRoot.updateAllData();
+ }
+ }
+ Connections {
+ target: I3.workspaces
+ enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
+ function onValuesChanged() {
+ delegateRoot.updateAllData();
+ }
+ }
+ Connections {
+ target: ExtWorkspaceService
+ enabled: root.useExtWorkspace
+ function onStateChanged() {
+ delegateRoot.updateAllData();
+ }
+ }
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
+ DMSService.addSubscription("extworkspace");
+ }
+ _updateBlurRegistration();
+ }
+
+ property bool _blurRegistered: false
+ readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
+
+ on_ShouldBlurChanged: _updateBlurRegistration()
+
+ function _updateBlurRegistration() {
+ if (_shouldBlur && !_blurRegistered) {
+ blurBarWindow.registerBlurWidget(visualBackground);
+ _blurRegistered = true;
+ } else if (!_shouldBlur && _blurRegistered) {
+ if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
+ blurBarWindow.unregisterBlurWidget(visualBackground);
+ _blurRegistered = false;
+ }
+ }
+
+ Component.onDestruction: {
+ if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
+ blurBarWindow.unregisterBlurWidget(visualBackground);
+ }
+}