From 5d94c0a7d44a2255b81815a52a7056a94a39842d Mon Sep 17 00:00:00 2001 From: nippy Date: Sat, 18 Apr 2026 13:49:56 +0200 Subject: update Raveos themes --- .../Modules/DankBar/Widgets/AppsDock.qml | 1011 ++++++++++ .../DankBar/Widgets/AppsDockContextMenu.qml | 486 +++++ .../DankBar/Widgets/AppsDockOverflowButton.qml | 76 + .../Modules/DankBar/Widgets/AudioVisualization.qml | 94 + .../quickshell/Modules/DankBar/Widgets/Battery.qml | 165 ++ .../Modules/DankBar/Widgets/CapsLockIndicator.qml | 62 + .../Modules/DankBar/Widgets/ClipboardButton.qml | 314 ++++ .../quickshell/Modules/DankBar/Widgets/Clock.qml | 331 ++++ .../Modules/DankBar/Widgets/ColorPicker.qml | 37 + .../DankBar/Widgets/ControlCenterButton.qml | 830 ++++++++ .../Modules/DankBar/Widgets/CpuMonitor.qml | 146 ++ .../Modules/DankBar/Widgets/CpuTemperature.qml | 140 ++ .../Modules/DankBar/Widgets/DWLLayout.qml | 110 ++ .../Modules/DankBar/Widgets/DiskUsage.qml | 255 +++ .../Modules/DankBar/Widgets/FocusedApp.qml | 317 ++++ .../Modules/DankBar/Widgets/GpuTemperature.qml | 224 +++ .../Modules/DankBar/Widgets/IdleInhibitor.qml | 36 + .../Modules/DankBar/Widgets/KeyboardLayoutName.qml | 268 +++ .../Modules/DankBar/Widgets/LauncherButton.qml | 123 ++ .../quickshell/Modules/DankBar/Widgets/Media.qml | 502 +++++ .../Modules/DankBar/Widgets/NetworkMonitor.qml | 147 ++ .../Modules/DankBar/Widgets/NotepadButton.qml | 367 ++++ .../DankBar/Widgets/NotificationCenterButton.qml | 40 + .../Modules/DankBar/Widgets/PowerMenuButton.qml | 25 + .../Modules/DankBar/Widgets/PrivacyIndicator.qml | 247 +++ .../Modules/DankBar/Widgets/RamMonitor.qml | 175 ++ .../Modules/DankBar/Widgets/RunningApps.qml | 902 +++++++++ .../Modules/DankBar/Widgets/SystemTrayBar.qml | 1893 +++++++++++++++++++ .../Modules/DankBar/Widgets/SystemUpdate.qml | 187 ++ .../quickshell/Modules/DankBar/Widgets/Vpn.qml | 141 ++ .../quickshell/Modules/DankBar/Widgets/Weather.qml | 80 + .../Modules/DankBar/Widgets/WorkspaceSwitcher.qml | 1981 ++++++++++++++++++++ 32 files changed, 11712 insertions(+) create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDock.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockOverflowButton.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CapsLockIndicator.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ClipboardButton.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Clock.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ColorPicker.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuMonitor.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuTemperature.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DWLLayout.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DiskUsage.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/FocusedApp.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/GpuTemperature.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/IdleInhibitor.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/LauncherButton.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Media.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NetworkMonitor.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotepadButton.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PrivacyIndicator.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RamMonitor.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RunningApps.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Vpn.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Weather.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets') 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); + } +} -- cgit v1.3