diff --git a/Assets/settings-default.json b/Assets/settings-default.json index ec5ba7d7..ac12bc70 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -1,5 +1,5 @@ { - "settingsVersion": 4, + "settingsVersion": 5, "bar": { "position": "top", "backgroundOpacity": 1, @@ -28,7 +28,7 @@ ], "right": [ { - "id": "ScreenRecorderIndicator" + "id": "ScreenRecorder" }, { "id": "Tray" @@ -58,7 +58,7 @@ "id": "Clock" }, { - "id": "SidePanelToggle" + "id": "ControlCenter" } ] } @@ -132,7 +132,8 @@ "respectExpireTimeout": false, "lowUrgencyDuration": 3, "normalUrgencyDuration": 8, - "criticalUrgencyDuration": 15 + "criticalUrgencyDuration": 15, + "enableOSD": true }, "audio": { "volumeStep": 5, diff --git a/Commons/Icons.qml b/Commons/Icons.qml index 93ed5d96..70f0934e 100644 --- a/Commons/Icons.qml +++ b/Commons/Icons.qml @@ -4,7 +4,6 @@ import QtQuick import QtQuick.Controls import Quickshell import qs.Commons -import qs.Commons.IconsSets Singleton { id: root diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 251a0c82..06a42d3e 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -53,7 +53,8 @@ Singleton { // This should only be activated once when the settings structure has changed // Then it should be commented out again, regular users don't need to generate // default settings on every start - //generateDefaultSettings() + // TODO: automate this someday! + // generateDefaultSettings() // Patch-in the local default, resolved to user's home adapter.general.avatarImage = defaultAvatar @@ -113,7 +114,7 @@ Singleton { JsonAdapter { id: adapter - property int settingsVersion: 4 + property int settingsVersion: 5 // bar property JsonObject bar: JsonObject { @@ -142,7 +143,7 @@ Singleton { "id": "Workspace" }] property list right: [{ - "id": "ScreenRecorderIndicator" + "id": "ScreenRecorder" }, { "id": "Tray" }, { @@ -162,7 +163,7 @@ Singleton { }, { "id": "Clock" }, { - "id": "SidePanelToggle" + "id": "ControlCenter" }] } } @@ -254,6 +255,7 @@ Singleton { property int lowUrgencyDuration: 3 property int normalUrgencyDuration: 8 property int criticalUrgencyDuration: 15 + property bool enableOSD: true } // audio @@ -381,15 +383,25 @@ Singleton { const sections = ["left", "center", "right"] // ----------------- - // 1st. check our settings are not super old, when we only had the widget type as a plain string + // 1st. convert old widget id to new id for (var s = 0; s < sections.length; s++) { const sectionName = sections[s] for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) { var widget = adapter.bar.widgets[sectionName][i] - if (typeof widget === "string") { - adapter.bar.widgets[sectionName][i] = { - "id": widget - } + + switch (widget.id) { + case "DarkModeToggle": + widget.id = "DarkMode" + break + case "PowerToggle": + widget.id = "SessionMenu" + break + case "ScreenRecorderIndicator": + widget.id = "ScreenRecorder" + break + case "SidePanelToggle": + widget.id = "ControlCenter" + break } } } @@ -403,8 +415,8 @@ Singleton { for (var i = widgets.length - 1; i >= 0; i--) { var widget = widgets[i] if (!BarWidgetRegistry.hasWidget(widget.id)) { - widgets.splice(i, 1) Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`) + widgets.splice(i, 1) } } } diff --git a/Commons/IconsSets/TablerIcons.qml b/Commons/TablerIcons.qml similarity index 99% rename from Commons/IconsSets/TablerIcons.qml rename to Commons/TablerIcons.qml index adb79ce6..75d8f0c6 100644 --- a/Commons/IconsSets/TablerIcons.qml +++ b/Commons/TablerIcons.qml @@ -56,6 +56,9 @@ Singleton { "keep-awake-on": "mug", "keep-awake-off": "mug-off", "disc": "disc-filled", + "eye": "eye", + "pin": "pin", + "unpin": "pinned-off", "image": "photo", "dark-mode": "contrast-filled", "camera-video": "video", @@ -119,28 +122,6 @@ Singleton { "bt-device-watch": "device-watch", "bt-device-speaker": "device-speaker", "bt-device-tv": "device-tv", - "filepicker-folder": "folder", - "filepicker-refresh": "refresh", - "filepicker-close": "x", - "filepicker-arrow-left": "arrow-left", - "filepicker-arrow-up": "arrow-up", - "filepicker-home": "home", - "filepicker-layout-grid": "layout-grid", - "filepicker-list": "list", - "filepicker-search": "search", - "filepicker-x": "x", - "filepicker-photo": "photo", - "filepicker-check": "check", - "filepicker-file-text": "file-text", - "filepicker-video": "video", - "filepicker-music": "music", - "filepicker-archive": "archive", - "filepicker-table": "table", - "filepicker-presentation": "presentation", - "filepicker-code": "code", - "filepicker-settings": "settings", - "filepicker-file": "file", - "filepicker-text": "file-text", "noctalia": "noctalia" } diff --git a/Commons/AppIcons.qml b/Commons/ThemeIcons.qml similarity index 100% rename from Commons/AppIcons.qml rename to Commons/ThemeIcons.qml diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 7f588e73..4dd653b2 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -3,7 +3,6 @@ import Quickshell import Quickshell.Wayland import qs.Commons import qs.Services -import qs.Modules.SettingsPanel import qs.Widgets Variants { diff --git a/Modules/BluetoothPanel/BluetoothDevicesList.qml b/Modules/Bar/Bluetooth/BluetoothDevicesList.qml similarity index 100% rename from Modules/BluetoothPanel/BluetoothDevicesList.qml rename to Modules/Bar/Bluetooth/BluetoothDevicesList.qml diff --git a/Modules/BluetoothPanel/BluetoothPanel.qml b/Modules/Bar/Bluetooth/BluetoothPanel.qml similarity index 100% rename from Modules/BluetoothPanel/BluetoothPanel.qml rename to Modules/Bar/Bluetooth/BluetoothPanel.qml diff --git a/Modules/Calendar/Calendar.qml b/Modules/Bar/Calendar/CalendarPanel.qml similarity index 100% rename from Modules/Calendar/Calendar.qml rename to Modules/Bar/Calendar/CalendarPanel.qml diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/Bar/WiFi/WiFiPanel.qml similarity index 100% rename from Modules/WiFiPanel/WiFiPanel.qml rename to Modules/Bar/WiFi/WiFiPanel.qml diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 5e6eb2f8..933d75f4 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -31,6 +31,7 @@ Item { } readonly property string windowTitle: CompositorService.getFocusedWindowTitle() + readonly property bool hasActiveWindow: windowTitle !== "" readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon @@ -52,7 +53,7 @@ Item { implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling) implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling) - visible: windowTitle !== "" + visible: hasActiveWindow function calculatedVerticalHeight() { // Use standard widget height like other widgets @@ -86,7 +87,7 @@ Item { try { const idValue = focusedWindow.appId const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue) - const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase()) + const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase()) if (iconResult && iconResult !== "") { return iconResult } @@ -102,7 +103,7 @@ Item { if (activeToplevel.appId) { const idValue2 = activeToplevel.appId const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2) - const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase()) + const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase()) if (iconResult2 && iconResult2 !== "") { return iconResult2 } @@ -159,7 +160,7 @@ Item { Layout.preferredWidth: Style.capsuleHeight * 0.75 * scaling Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling Layout.alignment: Qt.AlignVCenter - visible: windowTitle !== "" && showIcon + visible: hasActiveWindow && showIcon IconImage { id: windowIcon @@ -217,7 +218,7 @@ Item { anchors.centerIn: parent width: parent.width - Style.marginXS * scaling * 2 height: parent.height - Style.marginXS * scaling * 2 - visible: barPosition === "left" || barPosition === "right" + visible: (barPosition === "left" || barPosition === "right") && hasActiveWindow // Window icon Item { diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index e81ef351..652660c0 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -1,10 +1,10 @@ import QtQuick import Quickshell import qs.Commons -import qs.Modules.SettingsPanel +import qs.Modules.Bar.Extras +import qs.Modules.Settings import qs.Services import qs.Widgets -import qs.Modules.Bar.Extras Item { id: root diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/ControlCenter.qml similarity index 89% rename from Modules/Bar/Widgets/SidePanelToggle.qml rename to Modules/Bar/Widgets/ControlCenter.qml index f2336f69..c1d348f7 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/ControlCenter.qml @@ -43,7 +43,7 @@ NIconButton { colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary colorBorder: Color.transparent colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent - onClicked: PanelService.getPanel("sidePanel")?.toggle(this) + onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this) onRightClicked: PanelService.getPanel("settingsPanel")?.toggle() IconImage { @@ -52,9 +52,11 @@ NIconButton { width: root.width * 0.8 height: width source: { - if (customIconPath !== "") return customIconPath; - if (useDistroLogo) return DistroLogoService.osLogo; - return ""; + if (customIconPath !== "") + return customIconPath + if (useDistroLogo) + return DistroLogoService.osLogo + return "" } visible: source !== "" smooth: true diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index a2343238..3973b4cd 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -5,7 +5,7 @@ import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets -import qs.Modules.SettingsPanel +import qs.Modules.Settings import qs.Modules.Bar.Extras Item { diff --git a/Modules/Bar/Widgets/DarkModeToggle.qml b/Modules/Bar/Widgets/DarkMode.qml similarity index 100% rename from Modules/Bar/Widgets/DarkModeToggle.qml rename to Modules/Bar/Widgets/DarkMode.qml diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 6d55d481..e8b3fd90 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -100,7 +100,7 @@ Item { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying + active: showVisualizer && visualizerType == "linear" z: 0 sourceComponent: LinearSpectrum { @@ -115,7 +115,7 @@ Item { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying + active: showVisualizer && visualizerType == "mirrored" z: 0 sourceComponent: MirroredSpectrum { @@ -130,7 +130,7 @@ Item { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying + active: showVisualizer && visualizerType == "wave" z: 0 sourceComponent: WaveSpectrum { diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 6e1eda5c..41e5dbff 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -3,7 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import qs.Commons -import qs.Modules.SettingsPanel +import qs.Modules.Settings import qs.Services import qs.Widgets import qs.Modules.Bar.Extras diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 840224c2..b1801dd5 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -4,7 +4,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Modules.SettingsPanel +import qs.Modules.Settings import qs.Services import qs.Widgets diff --git a/Modules/Bar/Widgets/ScreenRecorderIndicator.qml b/Modules/Bar/Widgets/ScreenRecorder.qml similarity index 100% rename from Modules/Bar/Widgets/ScreenRecorderIndicator.qml rename to Modules/Bar/Widgets/ScreenRecorder.qml diff --git a/Modules/Bar/Widgets/PowerToggle.qml b/Modules/Bar/Widgets/SessionMenu.qml similarity index 83% rename from Modules/Bar/Widgets/PowerToggle.qml rename to Modules/Bar/Widgets/SessionMenu.qml index 9df6c452..38ee93dd 100644 --- a/Modules/Bar/Widgets/PowerToggle.qml +++ b/Modules/Bar/Widgets/SessionMenu.qml @@ -14,10 +14,10 @@ NIconButton { compact: (Settings.data.bar.density === "compact") baseSize: Style.capsuleHeight icon: "power" - tooltipText: "Power panel" + tooltipText: "Session menu" colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) colorFg: Color.mError colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("powerPanel")?.toggle() + onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle() } diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 71c11f67..63e7326d 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -68,7 +68,7 @@ Rectangle { anchors.centerIn: parent width: parent.width height: parent.height - source: AppIcons.iconForAppId(taskbarItem.modelData.appId) + source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId) smooth: true asynchronous: true } diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index e39a23f1..03ac13c9 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -3,7 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import qs.Commons -import qs.Modules.SettingsPanel +import qs.Modules.Settings import qs.Services import qs.Widgets import qs.Modules.Bar.Extras diff --git a/Modules/Bar/Widgets/WallpaperSelector.qml b/Modules/Bar/Widgets/WallpaperSelector.qml index 8743be2d..05163d2f 100644 --- a/Modules/Bar/Widgets/WallpaperSelector.qml +++ b/Modules/Bar/Widgets/WallpaperSelector.qml @@ -19,5 +19,5 @@ NIconButton { colorFg: Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("wallpaperSelector")?.toggle(this) + onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this) } diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 628b17e9..1721c30d 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -56,6 +56,10 @@ Item { property int horizontalPadding: Math.round(Style.marginS * scaling) property int spacingBetweenPills: Math.round(Style.marginXS * scaling) + // Wheel scroll handling + property int wheelAccumulatedDelta: 0 + property bool wheelCooldown: false + signal workspaceChanged(int workspaceId, color accentColor) implicitWidth: isVertical ? Math.round(Style.barHeight * scaling) : computeWidth() @@ -95,6 +99,28 @@ Item { return Math.round(total) } + function getFocusedLocalIndex() { + for (var i = 0; i < localWorkspaces.count; i++) { + if (localWorkspaces.get(i).isFocused === true) + return i + } + return -1 + } + + function switchByOffset(offset) { + if (localWorkspaces.count === 0) + return + var current = getFocusedLocalIndex() + if (current < 0) + current = 0 + var next = (current + offset) % localWorkspaces.count + if (next < 0) + next = localWorkspaces.count - 1 + const ws = localWorkspaces.get(next) + if (ws && ws.idx !== undefined) + CompositorService.switchToWorkspace(ws.idx) + } + Component.onCompleted: { refreshWorkspaces() } @@ -185,6 +211,46 @@ Item { anchors.verticalCenter: parent.verticalCenter } + // Debounce timer for wheel interactions + Timer { + id: wheelDebounce + interval: 150 + repeat: false + onTriggered: { + root.wheelCooldown = false + root.wheelAccumulatedDelta = 0 + } + } + + // Scroll to switch workspaces + WheelHandler { + id: wheelHandler + target: root + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: function (event) { + if (root.wheelCooldown) + return + // Prefer vertical delta, fall back to horizontal if needed + var dy = event.angleDelta.y + var dx = event.angleDelta.x + var useDy = Math.abs(dy) >= Math.abs(dx) + var delta = useDy ? dy : dx + // One notch is typically 120 + root.wheelAccumulatedDelta += delta + var step = 120 + if (Math.abs(root.wheelAccumulatedDelta) >= step) { + var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1 + // For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign) + // For horizontal layout, same mapping using vertical wheel + root.switchByOffset(direction) + root.wheelCooldown = true + wheelDebounce.restart() + root.wheelAccumulatedDelta = 0 + event.accepted = true + } + } + } + // Horizontal layout for top/bottom bars Row { id: pillRow diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/ControlCenter/Cards/MediaCard.qml similarity index 70% rename from Modules/SidePanel/Cards/MediaCard.qml rename to Modules/ControlCenter/Cards/MediaCard.qml index f8945df1..5e4ad4f8 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/ControlCenter/Cards/MediaCard.qml @@ -32,12 +32,12 @@ NBox { color: Color.mPrimary Layout.alignment: Qt.AlignHCenter } - NText { - text: "No media player detected" - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + // NText { + // text: "No media player detected" + // color: Color.mOnSurfaceVariant + // Layout.alignment: Qt.AlignHCenter + // } Item { Layout.fillWidth: true Layout.fillHeight: true @@ -51,91 +51,71 @@ NBox { visible: MediaService.currentPlayer && MediaService.canPlay spacing: Style.marginM * scaling - // Player selector - ComboBox { - id: playerSelector + // Player selector using NContextMenu + Rectangle { + id: playerSelectorButton Layout.fillWidth: true - Layout.preferredHeight: Style.barHeight * 0.83 * scaling + Layout.preferredHeight: Style.barHeight * scaling visible: MediaService.getAvailablePlayers().length > 1 - model: MediaService.getAvailablePlayers() - textRole: "identity" - currentIndex: MediaService.selectedPlayerIndex + radius: Style.radiusM * scaling + color: Color.transparent - background: Rectangle { - visible: false - // implicitWidth: 120 * scaling - // implicitHeight: 30 * scaling - color: Color.transparent - border.color: playerSelector.activeFocus ? Color.mSecondary : Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - radius: Style.radiusM * scaling - } + property var currentPlayer: MediaService.getAvailablePlayers()[MediaService.selectedPlayerIndex] - contentItem: NText { - visible: false - leftPadding: Style.marginM * scaling - rightPadding: playerSelector.indicator.width + playerSelector.spacing - text: playerSelector.displayText - font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurface - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } + RowLayout { + anchors.fill: parent + spacing: Style.marginS * scaling - indicator: NIcon { - x: playerSelector.width - width - y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 - icon: "caret-down" - font.pointSize: Style.fontSizeXXL * scaling - color: Color.mOnSurface - horizontalAlignment: Text.AlignRight - } - - popup: Popup { - id: popup - x: playerSelector.width * 0.5 - y: playerSelector.height * 0.75 - width: playerSelector.width * 0.5 - implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginM * scaling) - padding: Style.marginS * scaling - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: playerSelector.popup.visible ? playerSelector.delegateModel : null - currentIndex: playerSelector.highlightedIndex - ScrollIndicator.vertical: ScrollIndicator {} + NIcon { + icon: "caret-down" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mOnSurfaceVariant } - background: Rectangle { - color: Color.mSurface - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - radius: Style.radiusXS * scaling + NText { + text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : "" + font.pointSize: Style.fontSizeXS * scaling + color: Color.mOnSurfaceVariant + Layout.fillWidth: true } } - delegate: ItemDelegate { - width: playerSelector.width - contentItem: NText { - text: modelData.identity - font.pointSize: Style.fontSizeS * scaling - color: highlighted ? Color.mSurface : Color.mOnSurface - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - highlighted: playerSelector.highlightedIndex === index + MouseArea { + id: playerSelectorMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor - background: Rectangle { - width: popup.width - Style.marginS * scaling * 2 - color: highlighted ? Color.mSecondary : Color.transparent - radius: Style.radiusXS * scaling + onClicked: { + // Create menu items from available players + var menuItems = [] + var players = MediaService.getAvailablePlayers() + for (var i = 0; i < players.length; i++) { + menuItems.push({ + "label": players[i].identity, + "action": i.toString(), + "icon": "disc", + "enabled": true, + "visible": true + }) + } + playerContextMenu.model = menuItems + playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height) } } - onActivated: { - MediaService.selectedPlayerIndex = currentIndex - MediaService.updateCurrentPlayer() + NContextMenu { + id: playerContextMenu + parent: root + width: 200 * scaling + + onTriggered: function (action) { + var index = parseInt(action) + if (!isNaN(index)) { + MediaService.selectedPlayerIndex = index + MediaService.updateCurrentPlayer() + } + } } } @@ -193,7 +173,7 @@ NBox { NText { visible: MediaService.trackArtist !== "" text: MediaService.trackArtist - color: Color.mOnSurface + color: Color.mPrimary font.pointSize: Style.fontSizeXS * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -324,7 +304,7 @@ NBox { } Loader { - active: Settings.data.audio.visualizerType == "linear" && MediaService.isPlaying + active: Settings.data.audio.visualizerType == "linear" Layout.alignment: Qt.AlignHCenter sourceComponent: LinearSpectrum { @@ -337,7 +317,7 @@ NBox { } Loader { - active: Settings.data.audio.visualizerType == "mirrored" && MediaService.isPlaying + active: Settings.data.audio.visualizerType == "mirrored" Layout.alignment: Qt.AlignHCenter sourceComponent: MirroredSpectrum { @@ -350,7 +330,7 @@ NBox { } Loader { - active: Settings.data.audio.visualizerType == "wave" && MediaService.isPlaying + active: Settings.data.audio.visualizerType == "wave" Layout.alignment: Qt.AlignHCenter sourceComponent: WaveSpectrum { diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/ControlCenter/Cards/PowerProfilesCard.qml similarity index 100% rename from Modules/SidePanel/Cards/PowerProfilesCard.qml rename to Modules/ControlCenter/Cards/PowerProfilesCard.qml diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/ControlCenter/Cards/ProfileCard.qml similarity index 92% rename from Modules/SidePanel/Cards/ProfileCard.qml rename to Modules/ControlCenter/Cards/ProfileCard.qml index b4771315..d7aa4a4e 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/ControlCenter/Cards/ProfileCard.qml @@ -4,8 +4,8 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Widgets -import qs.Modules.SettingsPanel -import qs.Modules.SidePanel +import qs.Modules.Settings +import qs.Modules.ControlCenter import qs.Commons import qs.Services import qs.Widgets @@ -66,10 +66,10 @@ NBox { NIconButton { id: powerButton icon: "power" - tooltipText: "Power panel" + tooltipText: "Session Menu" onClicked: { - powerPanel.open() - sidePanel.close() + sessionMenuPanel.open() + controlCenterPanel.close() } } @@ -78,7 +78,7 @@ NBox { icon: "close" tooltipText: "Close side panel" onClicked: { - sidePanel.close() + controlCenterPanel.close() } } } diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/ControlCenter/Cards/SystemMonitorCard.qml similarity index 100% rename from Modules/SidePanel/Cards/SystemMonitorCard.qml rename to Modules/ControlCenter/Cards/SystemMonitorCard.qml diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/ControlCenter/Cards/UtilitiesCard.qml similarity index 90% rename from Modules/SidePanel/Cards/UtilitiesCard.qml rename to Modules/ControlCenter/Cards/UtilitiesCard.qml index ad0dde03..9bfae591 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/ControlCenter/Cards/UtilitiesCard.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Modules.SettingsPanel +import qs.Modules.Settings import qs.Services import qs.Widgets @@ -33,8 +33,8 @@ NBox { ScreenRecorderService.toggleRecording() // If we were not recording and we just initiated a start, close the panel if (!ScreenRecorderService.isRecording) { - var panel = PanelService.getPanel("sidePanel") - panel && panel.close() + var panel = PanelService.getPanel("controlCenterPanel") + panel?.close() } } } @@ -55,7 +55,7 @@ NBox { visible: Settings.data.wallpaper.enabled icon: "wallpaper-selector" tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper." - onClicked: PanelService.getPanel("wallpaperSelector")?.toggle(this) + onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this) onRightClicked: WallpaperService.setRandomWallpaper() } diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/ControlCenter/Cards/WeatherCard.qml similarity index 100% rename from Modules/SidePanel/Cards/WeatherCard.qml rename to Modules/ControlCenter/Cards/WeatherCard.qml diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml similarity index 98% rename from Modules/SidePanel/SidePanel.qml rename to Modules/ControlCenter/ControlCenterPanel.qml index bfd4e850..de1e7753 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import qs.Modules.SidePanel.Cards +import qs.Modules.ControlCenter.Cards import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 11f633f5..f6135c37 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -34,14 +34,6 @@ Variants { } } - // Also listen to model changes (for ObjectModel) - Connections { - target: ToplevelManager ? ToplevelManager.toplevels : null - function onCountChanged() { - updateDockApps() - } - } - // Update dock apps when pinned apps change Connections { target: Settings.data.dock @@ -108,19 +100,28 @@ Variants { const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : [] const pinnedApps = Settings.data.dock.pinnedApps || [] const combined = [] + const processedAppIds = new Set() - // First, add pinned apps (both running and non-running) in their pinned order + // Strategy: Maintain app positions as much as possible + // 1. First pass: Add all running apps (both pinned and non-pinned) in their current order + runningApps.forEach(toplevel => { + if (toplevel && toplevel.appId) { + const isPinned = pinnedApps.includes(toplevel.appId) + const appType = isPinned ? "pinned-running" : "running" + + combined.push({ + "type": appType, + "toplevel": toplevel, + "appId": toplevel.appId, + "title": toplevel.title + }) + processedAppIds.add(toplevel.appId) + } + }) + + // 2. Second pass: Add non-running pinned apps at the end pinnedApps.forEach(pinnedAppId => { - const runningApp = runningApps.find(toplevel => toplevel && toplevel.appId === pinnedAppId) - if (runningApp) { - // Pinned app that is currently running - combined.push({ - "type": "pinned-running", - "toplevel": runningApp, - "appId": runningApp.appId, - "title": runningApp.title - }) - } else { + if (!processedAppIds.has(pinnedAppId)) { // Pinned app that is not running combined.push({ "type": "pinned", @@ -131,21 +132,6 @@ Variants { } }) - // Then, add running apps that are not pinned - runningApps.forEach(toplevel => { - if (toplevel && toplevel.appId) { - const isPinned = pinnedApps.includes(toplevel.appId) - if (!isPinned) { - combined.push({ - "type": "running", - "toplevel": toplevel, - "appId": toplevel.appId, - "title": toplevel.title - }) - } - } - }) - dockApps = combined } @@ -355,7 +341,7 @@ Variants { function getAppIcon(appData): string { if (!appData || !appData.appId) return "" - return AppIcons.iconForAppId(appData.appId?.toLowerCase()) + return ThemeIcons.iconForAppId(appData.appId?.toLowerCase()) } RowLayout { @@ -462,13 +448,25 @@ Variants { id: contextMenu scaling: root.scaling onHoveredChanged: menuHovered = hovered - onRequestClose: contextMenu.hide() + onRequestClose: { + contextMenu.hide() + // Restart hide timer after menu action if auto-hide is enabled + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) { + hideTimer.restart() + } + } onAppClosed: root.updateDockApps // Force immediate dock update when app is closed onVisibleChanged: { if (visible) { root.currentContextMenu = contextMenu } else if (root.currentContextMenu === contextMenu) { root.currentContextMenu = null + // Reset menu hover state when menu becomes invisible + menuHovered = false + // Restart hide timer if conditions are met + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) { + hideTimer.restart() + } } } } @@ -501,11 +499,23 @@ Variants { } onClicked: function (mouse) { - // Close any existing context menu first - if (mouse.button !== Qt.RightButton || root.currentContextMenu !== contextMenu) { + if (mouse.button === Qt.RightButton) { + // If right-clicking on the same app with an open context menu, close it + if (root.currentContextMenu === contextMenu && contextMenu.visible) { + root.closeAllContextMenus() + return + } + // Close any other existing context menu first root.closeAllContextMenus() + // Hide tooltip when showing context menu + appTooltip.hide() + contextMenu.show(appButton, modelData.toplevel || modelData) + return } + // Close any existing context menu for non-right-click actions + root.closeAllContextMenus() + // Check if toplevel is still valid (not a stale reference) const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel) @@ -520,10 +530,6 @@ Variants { // Pinned app not running - launch it Quickshell.execDetached(["gtk-launch", modelData.appId]) } - } else if (mouse.button === Qt.RightButton) { - // Hide tooltip when showing context menu - appTooltip.hide() - contextMenu.show(appButton, modelData.toplevel || modelData) } } } diff --git a/Modules/Dock/DockMenu.qml b/Modules/Dock/DockMenu.qml index f4b78ae5..ff4cfa78 100644 --- a/Modules/Dock/DockMenu.qml +++ b/Modules/Dock/DockMenu.qml @@ -19,7 +19,7 @@ PopupWindow { signal requestClose - implicitWidth: 160 * scaling + implicitWidth: 140 * scaling implicitHeight: contextMenuColumn.implicitHeight + (Style.marginM * scaling * 2) color: Color.transparent visible: false @@ -75,7 +75,14 @@ PopupWindow { id: menuMouseArea anchors.fill: parent hoverEnabled: true - onClicked: root.hide() // Close when clicking on the background (outside menu content) + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: function (mouse) { + if (mouse.button === Qt.RightButton) { + root.hide() // Close on right-click + } else { + root.hide() // Close when clicking on the background (outside menu content) + } + } } Shortcut { @@ -86,7 +93,7 @@ PopupWindow { Rectangle { anchors.fill: parent - color: Color.mSurface + color: Color.mSurfaceVariant radius: Style.radiusS * scaling border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) @@ -105,11 +112,11 @@ PopupWindow { anchors.margins: Style.marginM * scaling spacing: 0 - // Activate/Focus item + // Focus item Rectangle { width: parent.width height: 32 * scaling - color: activateMouseArea.containsMouse ? Qt.alpha(Color.mSecondary, 0.2) : Color.transparent + color: activateMouseArea.containsMouse ? Color.mTertiary : Color.transparent radius: Style.radiusXS * scaling Row { @@ -121,20 +128,14 @@ PopupWindow { NIcon { icon: "eye" font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface + color: activateMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant anchors.verticalCenter: parent.verticalCenter } NText { - text: { - if (!root.toplevel) - return "Activate" - // Check if this toplevel is active by comparing with ToplevelManager.activeToplevel - const isActive = ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === root.toplevel - return isActive ? "Focus" : "Activate" - } + text: "Focus" font.pointSize: Style.fontSizeS * scaling - color: Color.mOnSurface + color: activateMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant anchors.verticalCenter: parent.verticalCenter } } @@ -149,7 +150,7 @@ PopupWindow { if (root.toplevel?.activate) { root.toplevel.activate() } - root.hide() + root.requestClose() } } } @@ -158,7 +159,7 @@ PopupWindow { Rectangle { width: parent.width height: 32 * scaling - color: pinMouseArea.containsMouse ? Qt.alpha(Color.mTertiary, 0.2) : Color.transparent + color: pinMouseArea.containsMouse ? Color.mTertiary : Color.transparent radius: Style.radiusXS * scaling Row { @@ -171,10 +172,10 @@ PopupWindow { icon: { if (!root.toplevel) return "pin" - return root.isAppPinned(root.toplevel.appId) ? "pinned-off" : "pin" + return root.isAppPinned(root.toplevel.appId) ? "unpin" : "pin" } font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface + color: pinMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant anchors.verticalCenter: parent.verticalCenter } @@ -185,7 +186,7 @@ PopupWindow { return root.isAppPinned(root.toplevel.appId) ? "Unpin" : "Pin" } font.pointSize: Style.fontSizeS * scaling - color: Color.mOnSurface + color: pinMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant anchors.verticalCenter: parent.verticalCenter } } @@ -200,7 +201,8 @@ PopupWindow { if (root.toplevel?.appId) { root.toggleAppPin(root.toplevel.appId) } - root.hide() + //root.hide() + root.requestClose() } } } @@ -209,7 +211,7 @@ PopupWindow { Rectangle { width: parent.width height: 32 * scaling - color: closeMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Color.transparent + color: closeMouseArea.containsMouse ? Color.mTertiary : Color.transparent radius: Style.radiusXS * scaling Row { @@ -219,16 +221,16 @@ PopupWindow { spacing: Style.marginS * scaling NIcon { - icon: "x" + icon: "close" font.pointSize: Style.fontSizeL * scaling - color: closeMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface + color: closeMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant anchors.verticalCenter: parent.verticalCenter } NText { text: "Close" font.pointSize: Style.fontSizeS * scaling - color: closeMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface + color: closeMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant anchors.verticalCenter: parent.verticalCenter } } @@ -253,6 +255,7 @@ PopupWindow { Logger.warn("DockMenu", "Cannot close app - invalid toplevel reference") } root.hide() + root.requestClose() } } } diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 8789ee25..ba1855e3 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -397,7 +397,7 @@ NPanel { sourceComponent: Component { IconImage { anchors.fill: parent - source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : "" + source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" visible: modelData.icon && source !== "" asynchronous: true } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index a9ac0235..16219f92 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -199,7 +199,7 @@ Loader { z: 10 Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear" + active: Settings.data.audio.visualizerType == "linear" anchors.centerIn: parent width: 160 * scaling height: 160 * scaling @@ -228,7 +228,7 @@ Loader { } Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored" + active: Settings.data.audio.visualizerType == "mirrored" anchors.centerIn: parent width: 160 * scaling height: 160 * scaling @@ -258,7 +258,7 @@ Loader { } Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave" + active: Settings.data.audio.visualizerType == "wave" anchors.centerIn: parent width: 160 * scaling height: 160 * scaling @@ -305,31 +305,6 @@ Loader { } } - Rectangle { - anchors.centerIn: parent - width: parent.width + 24 * scaling - height: parent.height + 24 * scaling - radius: width * 0.5 - color: Color.transparent - border.color: Qt.alpha(Color.mPrimary, 0.3) - border.width: Math.max(1, Style.borderM * scaling) - z: -1 - visible: !MediaService.isPlaying - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { - to: 1.1 - duration: 1500 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1.0 - duration: 1500 - easing.type: Easing.InOutQuad - } - } - } - NImageCircled { anchors.centerIn: parent width: 100 * scaling @@ -364,6 +339,7 @@ Loader { Rectangle { id: terminalBackground anchors.fill: parent + clip: true radius: Style.radiusM * scaling color: Qt.alpha(Color.mSurface, 0.9) border.color: Color.mPrimary @@ -558,48 +534,60 @@ Loader { } } - NText { - id: asterisksText - text: "*".repeat(passwordInput.text.length) - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - visible: passwordInput.activeFocus && !lockContext.unlockInProgress + // Container for asterisks and cursor to control positioning + Item { + Layout.fillWidth: true + Layout.preferredHeight: asterisksText.implicitHeight - SequentialAnimation { - id: typingEffect - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.01 - duration: 50 - } - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.0 - duration: 50 + NText { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + visible: passwordInput.activeFocus && !lockContext.unlockInProgress + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + wrapMode: Text.NoWrap + maximumLineCount: 1 + elide: Text.ElideRight + + SequentialAnimation { + id: typingEffect + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.01 + duration: 50 + } + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.0 + duration: 50 + } } } - } - Rectangle { - width: 8 * scaling - height: 20 * scaling - color: Color.mPrimary - visible: passwordInput.activeFocus - Layout.leftMargin: -Style.marginS * scaling - Layout.alignment: Qt.AlignVCenter + Rectangle { + width: 8 * scaling + height: 20 * scaling + color: Color.mPrimary + visible: passwordInput.activeFocus + anchors.left: asterisksText.right + anchors.leftMargin: Style.marginXS * scaling + anchors.verticalCenter: parent.verticalCenter - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 500 - } - NumberAnimation { - to: 0.0 - duration: 500 + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 500 + } + NumberAnimation { + to: 0.0 + duration: 500 + } } } } @@ -643,6 +631,7 @@ Loader { RowLayout { Layout.alignment: Qt.AlignRight Layout.bottomMargin: -10 * scaling + Layout.fillWidth: true Rectangle { Layout.preferredWidth: 120 * scaling Layout.preferredHeight: 40 * scaling @@ -731,6 +720,149 @@ Loader { } } + // ALARMING Easter Egg for long passwords + Item { + id: easterEggContainer + anchors.fill: parent + z: 1000 + + property bool easterEggTriggered: false + + // Monitor password length + Connections { + target: passwordInput + function onTextChanged() { + if (passwordInput.text.length >= 25) { + easterEggContainer.easterEggTriggered = true + } + } + function onActiveFocusChanged() { + if (!passwordInput.activeFocus) { + easterEggContainer.easterEggTriggered = false + } + } + } + + // Also reset when authentication starts + Connections { + target: lockContext + function onUnlockInProgressChanged() { + if (lockContext.unlockInProgress) { + easterEggContainer.easterEggTriggered = false + } + } + } + + // Scattered warning messages (game-style pop-ups) + Repeater { + model: easterEggContainer.easterEggTriggered && passwordInput.activeFocus && !lockContext.unlockInProgress ? 12 : 0 + + NText { + property var messages: ["BREACH DETECTED", "SECURITY ALERT", "SYSTEM COMPROMISED", "ANOMALY DETECTED", "FIREWALL BREACH", "DEFENSE FAILING", "16 // 16 // 16", "THE ATLAS SEES ALL", "SIMULATION DETECTED", "WAKE UP", "16 16 16 16 16", "KZZT... 16... KZZT", "ERROR ERROR ERROR", "THEY'RE WATCHING", "16 MINUTES REMAIN"] + + property real baseX: Math.random() * (parent.width - 300) + property real baseY: Math.random() * (parent.height - 80) + + text: messages[index % messages.length] + color: Color.mError + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + + x: baseX + y: baseY + + // Better random positioning avoiding center terminal + Component.onCompleted: { + var centerX = parent.width / 2 + var centerY = parent.height / 2 + var avoidRadius = 350 * scaling + + // If too close to center, push to random edge zones + var distanceFromCenter = Math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)) + if (distanceFromCenter < avoidRadius) { + // Pick a random edge zone + var zone = Math.floor(Math.random() * 4) + switch (zone) { + case 0: + // Top + x = Math.random() * parent.width + y = Math.random() * 100 * scaling + break + case 1: + // Right + x = parent.width - (50 + Math.random() * 200) * scaling + y = Math.random() * parent.height + break + case 2: + // Bottom + x = Math.random() * parent.width + y = parent.height - (50 + Math.random() * 100) * scaling + break + case 3: + // Left + x = Math.random() * 200 * scaling + y = Math.random() * parent.height + break + } + } + + // Add some random drift to make positioning more varied + x += (Math.random() - 0.5) * 100 * scaling + y += (Math.random() - 0.5) * 50 * scaling + + // Ensure we stay within bounds + x = Math.max(20 * scaling, Math.min(parent.width - 280 * scaling, x)) + y = Math.max(20 * scaling, Math.min(parent.height - 60 * scaling, y)) + } + + // Simple pop-in animation + SequentialAnimation on scale { + loops: Animation.Infinite + PauseAnimation { + duration: index * 400 + Math.random() * 1000 + } + NumberAnimation { + from: 0 + to: 1.2 + duration: 300 + easing.type: Easing.OutBack + } + NumberAnimation { + to: 1.0 + duration: 200 + } + PauseAnimation { + duration: 2000 + Math.random() * 3000 + } + NumberAnimation { + to: 0 + duration: 300 + } + PauseAnimation { + duration: 800 + Math.random() * 1200 + } + } + + // Gentle blinking effect + SequentialAnimation on opacity { + loops: Animation.Infinite + PauseAnimation { + duration: index * 200 + } + NumberAnimation { + to: 0.6 + duration: 400 + Math.random() * 300 + } + NumberAnimation { + to: 1.0 + duration: 300 + Math.random() * 200 + } + } + } + } + } + // Power buttons at bottom right RowLayout { anchors.right: parent.right @@ -759,7 +891,7 @@ Loader { Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.top - anchors.bottomMargin: 12 * scaling + anchors.bottomMargin: Style.marginM * scaling radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline @@ -810,7 +942,7 @@ Loader { Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.top - anchors.bottomMargin: 12 * scaling + anchors.bottomMargin: Style.marginM * scaling radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline @@ -862,7 +994,7 @@ Loader { Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.top - anchors.bottomMargin: 12 * scaling + anchors.bottomMargin: Style.marginM * scaling radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 1fe59c93..35f92d0a 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -12,7 +12,7 @@ import qs.Widgets NPanel { id: root - preferredWidth: 360 + preferredWidth: 380 preferredHeight: 480 panelKeyboardFocus: true @@ -37,7 +37,7 @@ NPanel { } NText { - text: "Notification history" + text: "Notifications" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml new file mode 100644 index 00000000..277bf1b9 --- /dev/null +++ b/Modules/OSD/OSD.qml @@ -0,0 +1,316 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +Loader { + id: windowLoader + active: false + + // OSD Type enum + enum Type { + Volume, + Brightness + } + + property int osdType: OSD.Type.Volume + readonly property real scaling: ScalingService.getScreenScale(Quickshell.screens[0]) + + // Volume properties + readonly property real currentVolume: AudioService.volume + readonly property bool isMuted: AudioService.muted + property bool firstVolumeReceived: false + property bool firstMuteReceived: false + + // Brightness properties + readonly property real currentBrightness: { + if (BrightnessService.monitors.length > 0) { + return BrightnessService.monitors[0].brightness || 0 + } + return 0 + } + property bool firstBrightnessReceived: false + + // Get appropriate icon based on OSD type + function getIcon() { + if (osdType === OSD.Type.Volume) { + if (AudioService.muted) { + return "volume-mute" + } + return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high" + } else { + // Brightness + var brightness = currentBrightness + return brightness <= 0.5 ? "brightness-low" : "brightness-high" + } + } + + // Get current value (0-1 range) + function getCurrentValue() { + if (osdType === OSD.Type.Volume) { + return isMuted ? 0 : currentVolume + } else { + return currentBrightness + } + } + + // Get display percentage + function getDisplayPercentage() { + if (osdType === OSD.Type.Volume) { + return isMuted ? "0%" : Math.round(currentVolume * 100) + "%" + } else { + return Math.round(currentBrightness * 100) + "%" + } + } + + // Get progress bar color + function getProgressColor() { + if (osdType === OSD.Type.Volume) { + return isMuted ? Color.mError : Color.mPrimary + } else { + return Color.mPrimary + } + } + + // Get icon color + function getIconColor() { + if (osdType === OSD.Type.Volume) { + return isMuted ? Color.mError : Color.mOnSurface + } else { + return Color.mOnSurface + } + } + + sourceComponent: PanelWindow { + id: panel + + screen: Quickshell.screens[0] // Use primary screen + + anchors { + top: true + } + + implicitWidth: 320 * windowLoader.scaling + implicitHeight: osdItem.height + + // Set margins based on bar position + margins.top: { + switch (Settings.data.bar.position) { + case "top": + return (Style.barHeight + Style.marginS) * windowLoader.scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * windowLoader.scaling : 0) + default: + return Style.marginL * windowLoader.scaling + } + } + + color: Color.transparent + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + exclusionMode: PanelWindow.ExclusionMode.Ignore + + Rectangle { + id: osdItem + + width: parent.width + height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * windowLoader.scaling) + radius: Style.radiusL * windowLoader.scaling + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(2, Style.borderM * windowLoader.scaling) + visible: false + opacity: 0 + scale: 0.7 + + anchors.horizontalCenter: parent.horizontalCenter + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Timer { + id: hideTimer + interval: 2000 + onTriggered: osdItem.hide() + } + + RowLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginM * windowLoader.scaling + spacing: Style.marginM * windowLoader.scaling + + NIcon { + icon: windowLoader.getIcon() + color: windowLoader.getIconColor() + font.pointSize: Style.fontSizeXL * windowLoader.scaling + Layout.alignment: Qt.AlignVCenter + } + + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginXS * windowLoader.scaling + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Math.round(6 * windowLoader.scaling) + radius: Math.round(3 * windowLoader.scaling) + color: Color.mSurfaceVariant + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * Math.min(1.0, windowLoader.getCurrentValue()) + radius: parent.radius + color: windowLoader.getProgressColor() + + Behavior on width { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + NText { + text: windowLoader.getDisplayPercentage() + color: Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeS * windowLoader.scaling + Layout.alignment: Qt.AlignVCenter + Layout.minimumWidth: Math.round(32 * windowLoader.scaling) + } + } + } + + function show() { + hideTimer.stop() + osdItem.visible = true + osdItem.opacity = 1 + osdItem.scale = 1.0 + hideTimer.start() + } + + function hide() { + hideTimer.stop() + osdItem.opacity = 0 + osdItem.scale = 0.7 + + Qt.callLater(function () { + osdItem.visible = false + windowLoader.active = false + }) + } + + function hideImmediately() { + hideTimer.stop() + osdItem.opacity = 0 + osdItem.scale = 0.7 + osdItem.visible = false + windowLoader.active = false + } + } + + function showOSD() { + osdItem.show() + } + } + + // Volume change monitoring + Connections { + target: AudioService + enabled: osdType === OSD.Type.Volume + + function onVolumeChanged() { + if (!firstVolumeReceived) { + firstVolumeReceived = true + } else { + showOSD() + } + } + + function onMutedChanged() { + if (!firstMuteReceived) { + firstMuteReceived = true + } else { + showOSD() + } + } + } + + // Brightness change monitoring + Connections { + target: BrightnessService + enabled: osdType === OSD.Type.Brightness + + function onMonitorsChanged() { + for (var i = 0; i < BrightnessService.monitors.length; i++) { + let monitor = BrightnessService.monitors[i] + monitor.brightnessUpdated.connect(windowLoader.onBrightnessChanged) + } + } + } + + Component.onCompleted: { + if (osdType === OSD.Type.Brightness) { + for (var i = 0; i < BrightnessService.monitors.length; i++) { + let monitor = BrightnessService.monitors[i] + monitor.brightnessUpdated.connect(windowLoader.onBrightnessChanged) + } + } + } + + function onBrightnessChanged(newBrightness) { + if (!firstBrightnessReceived) { + firstBrightnessReceived = true + } else { + showOSD() + } + } + + // Signal to coordinate with other OSDs + signal osdShowing + + function showOSD() { + // Check if OSD is enabled in settings + if (!Settings.data.notifications.enableOSD) { + return + } + + osdShowing() // Notify other OSDs to hide + windowLoader.active = true + if (windowLoader.item) { + windowLoader.item.showOSD() + } + } + + function hideOSD() { + if (windowLoader.item && windowLoader.item.osdItem) { + windowLoader.item.osdItem.hideImmediately() + } else if (windowLoader.active) { + // If window exists but osdItem isn't ready, just deactivate the loader + windowLoader.active = false + } + } +} diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/SessionMenu/SessionMenu.qml similarity index 99% rename from Modules/PowerPanel/PowerPanel.qml rename to Modules/SessionMenu/SessionMenu.qml index df2381ce..c3de9a59 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/SessionMenu/SessionMenu.qml @@ -263,7 +263,7 @@ NPanel { Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling NText { - text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power panel" + text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Session Menu" font.weight: Style.fontWeightBold font.pointSize: Style.fontSizeL * scaling color: timerActive ? Color.mPrimary : Color.mOnSurface diff --git a/Modules/SettingsPanel/Bar/BarSectionEditor.qml b/Modules/Settings/Bar/BarSectionEditor.qml similarity index 90% rename from Modules/SettingsPanel/Bar/BarSectionEditor.qml rename to Modules/Settings/Bar/BarSectionEditor.qml index aa006544..39dc62e3 100644 --- a/Modules/SettingsPanel/Bar/BarSectionEditor.qml +++ b/Modules/Settings/Bar/BarSectionEditor.qml @@ -20,6 +20,7 @@ NBox { signal removeWidget(string section, int index) signal reorderWidget(string section, int fromIndex, int toIndex) signal updateWidgetSettings(string section, int index, var settings) + signal moveWidget(string fromSection, int index, string toSection) signal dragPotentialStarted signal dragPotentialEnded @@ -125,6 +126,7 @@ NBox { Repeater { model: widgetModel + delegate: Rectangle { id: widgetItem required property int index @@ -158,6 +160,51 @@ NBox { } } + // Context menu for moving widget to other sections + NContextMenu { + id: contextMenu + parent: Overlay.overlay + width: 240 * scaling + model: [{ + "label": "Move to left section", + "action": "left", + "icon": "arrow-bar-to-left", + "visible": root.sectionId !== "left" + }, { + "label": "Move to center section", + "action": "center", + "icon": "layout-columns", + "visible": root.sectionId !== "center" + }, { + "label": "Move to right section", + "action": "right", + "icon": "arrow-bar-to-right", + "visible": root.sectionId !== "right" + }] + + onTriggered: action => root.moveWidget(root.sectionId, index, action) + } + + // Update the MouseArea to use the new context menu + MouseArea { + id: contextMouseArea + anchors.fill: parent + acceptedButtons: Qt.RightButton + z: -1 // Below the buttons but above background + + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + // Check if click is not on the buttons area + const localX = mouse.x + const buttonsStartX = parent.width - (parent.buttonsCount * parent.buttonsWidth) + + if (localX < buttonsStartX) { + // Use the helper function to open at mouse position + contextMenu.openAtItem(widgetItem, mouse.x, mouse.y) + } + } + } + } RowLayout { id: widgetContent anchors.centerIn: parent @@ -180,6 +227,7 @@ NBox { active: BarWidgetRegistry.widgetHasUserSettings(modelData.id) sourceComponent: NIconButton { icon: "settings" + tooltipText: "Widget settings" baseSize: miniButtonSize colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight) colorBg: Color.mOnSurface @@ -220,6 +268,7 @@ NBox { NIconButton { icon: "close" + tooltipText: "Remove widget" baseSize: miniButtonSize colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight) colorBg: Color.mOnSurface diff --git a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml similarity index 77% rename from Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml rename to Modules/Settings/Bar/BarWidgetSettingsDialog.qml index fe65650f..86344997 100644 --- a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml @@ -9,12 +9,15 @@ import "./WidgetSettings" as WidgetSettings // Widget Settings Dialog Component Popup { - id: settingsPopup + // Don't replace by root! + id: widgetSettings property int widgetIndex: -1 property var widgetData: null property string widgetId: "" + property bool isMasked: false + // Center popup in parent x: (parent.width - width) * 0.5 y: (parent.height - height) * 0.5 @@ -24,51 +27,34 @@ Popup { padding: Style.marginXL * scaling modal: true - background: Rectangle { - id: bgRect - color: Color.mSurface - radius: Style.radiusL * scaling - border.color: Color.mPrimary - border.width: Style.borderM * scaling - } - - // Load settings when popup opens with data onOpened: { + // Mark this popup has opened in the PanelService + PanelService.willOpenPopup(widgetSettings) + + // Load settings when popup opens with data if (widgetData && widgetId) { loadWidgetSettings() } } - function loadWidgetSettings() { - const widgetSettingsMap = { - "ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml", - "Battery": "WidgetSettings/BatterySettings.qml", - "Brightness": "WidgetSettings/BrightnessSettings.qml", - "Clock": "WidgetSettings/ClockSettings.qml", - "CustomButton": "WidgetSettings/CustomButtonSettings.qml", - "KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml", - "MediaMini": "WidgetSettings/MediaMiniSettings.qml", - "Microphone": "WidgetSettings/MicrophoneSettings.qml", - "NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml", - "Workspace": "WidgetSettings/WorkspaceSettings.qml", - "SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml", - "Spacer": "WidgetSettings/SpacerSettings.qml", - "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", - "Volume": "WidgetSettings/VolumeSettings.qml" - } - - const source = widgetSettingsMap[widgetId] - if (source) { - // Use setSource to pass properties at creation time - settingsLoader.setSource(source, { - "widgetData": widgetData, - "widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId] - }) - } + onClosed: { + PanelService.willClosePopup(widgetSettings) } - ColumnLayout { + background: Rectangle { + id: bgRect + + opacity: widgetSettings.isMasked ? 0 : 1.0 + color: Color.mSurface + radius: Style.radiusL * scaling + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderM * scaling) + } + + contentItem: ColumnLayout { id: content + + opacity: widgetSettings.isMasked ? 0 : 1.0 width: parent.width spacing: Style.marginM * scaling @@ -77,7 +63,7 @@ Popup { Layout.fillWidth: true NText { - text: `${settingsPopup.widgetId} Settings` + text: `${widgetSettings.widgetId} Settings` font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold color: Color.mPrimary @@ -86,7 +72,7 @@ Popup { NIconButton { icon: "close" - onClicked: settingsPopup.close() + onClicked: widgetSettings.close() } } @@ -117,7 +103,7 @@ Popup { NButton { text: "Cancel" outlined: true - onClicked: settingsPopup.close() + onClicked: widgetSettings.close() } NButton { @@ -126,11 +112,39 @@ Popup { onClicked: { if (settingsLoader.item && settingsLoader.item.saveSettings) { var newSettings = settingsLoader.item.saveSettings() - root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings) - settingsPopup.close() + root.updateWidgetSettings(sectionId, widgetSettings.widgetIndex, newSettings) + widgetSettings.close() } } } } } + + function loadWidgetSettings() { + const widgetSettingsMap = { + "ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml", + "Battery": "WidgetSettings/BatterySettings.qml", + "Brightness": "WidgetSettings/BrightnessSettings.qml", + "Clock": "WidgetSettings/ClockSettings.qml", + "ControlCenter": "WidgetSettings/ControlCenterSettings.qml", + "CustomButton": "WidgetSettings/CustomButtonSettings.qml", + "KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml", + "MediaMini": "WidgetSettings/MediaMiniSettings.qml", + "Microphone": "WidgetSettings/MicrophoneSettings.qml", + "NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml", + "Spacer": "WidgetSettings/SpacerSettings.qml", + "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", + "Volume": "WidgetSettings/VolumeSettings.qml", + "Workspace": "WidgetSettings/WorkspaceSettings.qml" + } + + const source = widgetSettingsMap[widgetId] + if (source) { + // Use setSource to pass properties at creation time + settingsLoader.setSource(source, { + "widgetData": widgetData, + "widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId] + }) + } + } } diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml b/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml rename to Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml b/Modules/Settings/Bar/WidgetSettings/BatterySettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml rename to Modules/Settings/Bar/WidgetSettings/BatterySettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml b/Modules/Settings/Bar/WidgetSettings/BrightnessSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml rename to Modules/Settings/Bar/WidgetSettings/BrightnessSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml b/Modules/Settings/Bar/WidgetSettings/ClockSettings.qml similarity index 98% rename from Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml rename to Modules/Settings/Bar/WidgetSettings/ClockSettings.qml index 689612e2..60e2794c 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml +++ b/Modules/Settings/Bar/WidgetSettings/ClockSettings.qml @@ -84,7 +84,7 @@ ColumnLayout { NHeader { label: "Clock display" - description: "Arrange your clock's layout. Click a token below to add it to the selected field." + description: "Customize your clock's display by adding tokens from the list below. To use the 12-hour format, you must include the 'AP' token." } RowLayout { diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml b/Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml similarity index 86% rename from Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml rename to Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml index df48506c..9c6ab6a9 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml +++ b/Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml @@ -38,14 +38,6 @@ ColumnLayout { } } - NFilePicker { - id: filePicker - title: "Select a custom icon" - onFileSelected: function (filePath) { - valueCustomIconPath = "file://" + filePath - } - } - RowLayout { spacing: Style.marginM * scaling @@ -65,10 +57,13 @@ ColumnLayout { NIcon { Layout.alignment: Qt.AlignVCenter icon: valueIcon - font.pointSize: Style.fontSizeXL * scaling + font.pointSize: Style.fontSizeXXL * 1.5 * scaling visible: valueIcon !== "" && valueCustomIconPath === "" } + } + RowLayout { + spacing: Style.marginM * scaling NButton { enabled: !valueUseDistroLogo text: "Browse Library" @@ -85,9 +80,15 @@ ColumnLayout { NIconPicker { id: iconPicker initialIcon: valueIcon - onIconSelected: function (iconName) { - valueIcon = iconName - valueCustomIconPath = "" - } + onIconSelected: iconName => { + valueIcon = iconName + valueCustomIconPath = "" + } } -} \ No newline at end of file + + NFilePicker { + id: filePicker + title: "Select a custom icon" + onAccepted: paths => valueCustomIconPath = paths[0] + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml b/Modules/Settings/Bar/WidgetSettings/CustomButtonSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml rename to Modules/Settings/Bar/WidgetSettings/CustomButtonSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/KeyboardLayoutSettings.qml b/Modules/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/KeyboardLayoutSettings.qml rename to Modules/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml b/Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml rename to Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml b/Modules/Settings/Bar/WidgetSettings/MicrophoneSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml rename to Modules/Settings/Bar/WidgetSettings/MicrophoneSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml b/Modules/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml rename to Modules/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SpacerSettings.qml b/Modules/Settings/Bar/WidgetSettings/SpacerSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/SpacerSettings.qml rename to Modules/Settings/Bar/WidgetSettings/SpacerSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml b/Modules/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml rename to Modules/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml b/Modules/Settings/Bar/WidgetSettings/VolumeSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml rename to Modules/Settings/Bar/WidgetSettings/VolumeSettings.qml diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml b/Modules/Settings/Bar/WidgetSettings/WorkspaceSettings.qml similarity index 100% rename from Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml rename to Modules/Settings/Bar/WidgetSettings/WorkspaceSettings.qml diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml similarity index 99% rename from Modules/SettingsPanel/SettingsPanel.qml rename to Modules/Settings/SettingsPanel.qml index 5993f902..418f4223 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland -import qs.Modules.SettingsPanel.Tabs as Tabs +import qs.Modules.Settings.Tabs as Tabs import qs.Commons import qs.Services import qs.Widgets @@ -21,7 +21,7 @@ NPanel { panelKeyboardFocus: true - draggable: true + draggable: !PanelService.hasOpenedPopup // Tabs enumeration, order is NOT relevant enum Tab { diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/Settings/Tabs/AboutTab.qml similarity index 100% rename from Modules/SettingsPanel/Tabs/AboutTab.qml rename to Modules/Settings/Tabs/AboutTab.qml diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/Settings/Tabs/AudioTab.qml similarity index 100% rename from Modules/SettingsPanel/Tabs/AudioTab.qml rename to Modules/Settings/Tabs/AudioTab.qml diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml similarity index 90% rename from Modules/SettingsPanel/Tabs/BarTab.qml rename to Modules/Settings/Tabs/BarTab.qml index c332092c..2049959c 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -5,7 +5,7 @@ import Quickshell import qs.Commons import qs.Services import qs.Widgets -import qs.Modules.SettingsPanel.Bar +import qs.Modules.Settings.Bar ColumnLayout { id: root @@ -219,6 +219,7 @@ ColumnLayout { onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index) onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) onDragPotentialStarted: root.handleDragStart() onDragPotentialEnded: root.handleDragEnd() } @@ -233,6 +234,7 @@ ColumnLayout { onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index) onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) onDragPotentialStarted: root.handleDragStart() onDragPotentialEnded: root.handleDragEnd() } @@ -247,6 +249,7 @@ ColumnLayout { onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index) onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) onDragPotentialStarted: root.handleDragStart() onDragPotentialEnded: root.handleDragEnd() } @@ -341,6 +344,25 @@ ColumnLayout { //Logger.log("BarTab", `Updated widget settings for ${settings.id} in ${section} section`) } + function _moveWidgetBetweenSections(fromSection, index, toSection) { + // Get the widget from the source section + if (index >= 0 && index < Settings.data.bar.widgets[fromSection].length) { + var widget = Settings.data.bar.widgets[fromSection][index] + + // Remove from source section + var sourceArray = Settings.data.bar.widgets[fromSection].slice() + sourceArray.splice(index, 1) + Settings.data.bar.widgets[fromSection] = sourceArray + + // Add to target section + var targetArray = Settings.data.bar.widgets[toSection].slice() + targetArray.push(widget) + Settings.data.bar.widgets[toSection] = targetArray + + //Logger.log("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`) + } + } + // Base list model for all combo boxes ListModel { id: availableWidgets diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml similarity index 100% rename from Modules/SettingsPanel/Tabs/ColorSchemeTab.qml rename to Modules/Settings/Tabs/ColorSchemeTab.qml diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml similarity index 100% rename from Modules/SettingsPanel/Tabs/DisplayTab.qml rename to Modules/Settings/Tabs/DisplayTab.qml diff --git a/Modules/SettingsPanel/Tabs/DockTab.qml b/Modules/Settings/Tabs/DockTab.qml similarity index 100% rename from Modules/SettingsPanel/Tabs/DockTab.qml rename to Modules/Settings/Tabs/DockTab.qml diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml similarity index 92% rename from Modules/SettingsPanel/Tabs/GeneralTab.qml rename to Modules/Settings/Tabs/GeneralTab.qml index 1036a4a3..0e0d8932 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/Settings/Tabs/GeneralTab.qml @@ -30,7 +30,7 @@ ColumnLayout { Layout.alignment: Qt.AlignTop } - NInputButton { + NTextInputButton { label: `${Quickshell.env("USER") || "user"}'s profile picture` description: "Your profile picture that appears throughout the interface." text: Settings.data.general.avatarImage @@ -39,18 +39,20 @@ ColumnLayout { buttonTooltip: "Browse for avatar image" onInputEditingFinished: Settings.data.general.avatarImage = text onButtonClicked: { - FilePickerService.open({ - "title": "Select Avatar Image", - "initialPath": Settings.data.general.avatarImage || Quickshell.env("HOME"), - "selectFiles": true, - "scaling": scaling, - "parent": root, - "onSelected": path => Settings.data.general.avatarImage = path - }) + filePicker.open() } } } + NFilePicker { + id: filePicker + pickerType: "file" + title: "Select avatar image" + initialPath: Settings.data.general.avatarImage.substr(0, Settings.data.general.avatarImage.lastIndexOf("/")) || Quickshell.env("HOME") + nameFilters: ["Image files (*.jpg *.jpeg *.png *.gif *.pnm *.bmp *.face)", "All files (*)"] + onAccepted: paths => Settings.data.general.avatarImage = paths[0] + } + NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginXL * scaling diff --git a/Modules/SettingsPanel/Tabs/HooksTab.qml b/Modules/Settings/Tabs/HooksTab.qml similarity index 100% rename from Modules/SettingsPanel/Tabs/HooksTab.qml rename to Modules/Settings/Tabs/HooksTab.qml diff --git a/Modules/SettingsPanel/Tabs/LauncherTab.qml b/Modules/Settings/Tabs/LauncherTab.qml similarity index 100% rename from Modules/SettingsPanel/Tabs/LauncherTab.qml rename to Modules/Settings/Tabs/LauncherTab.qml diff --git a/Modules/SettingsPanel/Tabs/LocationTab.qml b/Modules/Settings/Tabs/LocationTab.qml similarity index 98% rename from Modules/SettingsPanel/Tabs/LocationTab.qml rename to Modules/Settings/Tabs/LocationTab.qml index 2696abab..d0cc8d3a 100644 --- a/Modules/SettingsPanel/Tabs/LocationTab.qml +++ b/Modules/Settings/Tabs/LocationTab.qml @@ -48,7 +48,7 @@ ColumnLayout { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignRight Layout.alignment: Qt.AlignBottom - Layout.bottomMargin: 12 * scaling + Layout.bottomMargin: Style.marginM * scaling } } diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/Settings/Tabs/NetworkTab.qml similarity index 100% rename from Modules/SettingsPanel/Tabs/NetworkTab.qml rename to Modules/Settings/Tabs/NetworkTab.qml diff --git a/Modules/SettingsPanel/Tabs/NotificationsTab.qml b/Modules/Settings/Tabs/NotificationsTab.qml similarity index 95% rename from Modules/SettingsPanel/Tabs/NotificationsTab.qml rename to Modules/Settings/Tabs/NotificationsTab.qml index 1730156c..4f017951 100644 --- a/Modules/SettingsPanel/Tabs/NotificationsTab.qml +++ b/Modules/Settings/Tabs/NotificationsTab.qml @@ -39,6 +39,13 @@ ColumnLayout { onToggled: checked => Settings.data.notifications.doNotDisturb = checked } + NToggle { + label: "Enable on screen display" + description: "Show volume and brightness changes in real-time." + checked: Settings.data.notifications.enableOSD + onToggled: checked => Settings.data.notifications.enableOSD = checked + } + NComboBox { label: "Location" description: "Where notifications appear on screen." diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/Settings/Tabs/ScreenRecorderTab.qml similarity index 90% rename from Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml rename to Modules/Settings/Tabs/ScreenRecorderTab.qml index c2976301..f05e8dc7 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/Settings/Tabs/ScreenRecorderTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell import qs.Commons import qs.Services import qs.Widgets @@ -20,24 +21,15 @@ ColumnLayout { spacing: Style.marginS * scaling Layout.fillWidth: true - NInputButton { + NTextInputButton { label: "Output folder" description: "Folder where screen recordings will be saved." - placeholderText: "/home/xxx/Videos" + placeholderText: Quickshell.env("HOME") + "/Videos" text: Settings.data.screenRecorder.directory buttonIcon: "folder-open" buttonTooltip: "Browse for output folder" onInputEditingFinished: Settings.data.screenRecorder.directory = text - onButtonClicked: { - FilePickerService.open({ - "title": "Select Output Folder", - "initialPath": Settings.data.screenRecorder.directory || Quickshell.env("HOME") + "/Videos", - "selectFiles": false, - "scaling": scaling, - "parent": root, - "onSelected": path => Settings.data.screenRecorder.directory = path - }) - } + onButtonClicked: folderPicker.open() } ColumnLayout { @@ -261,4 +253,12 @@ ColumnLayout { Layout.topMargin: Style.marginXL * scaling Layout.bottomMargin: Style.marginXL * scaling } + + NFilePicker { + id: folderPicker + pickerType: "folder" + title: "Select output folder" + initialPath: Settings.data.screenRecorder.directory || Quickshell.env("HOME") + "/Videos" + onAccepted: paths => Settings.data.screenRecorder.directory = paths[0] + } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/Settings/Tabs/WallpaperTab.qml similarity index 86% rename from Modules/SettingsPanel/Tabs/WallpaperTab.qml rename to Modules/Settings/Tabs/WallpaperTab.qml index f2fce98f..13d039cc 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/Settings/Tabs/WallpaperTab.qml @@ -11,6 +11,8 @@ ColumnLayout { id: root spacing: Style.marginL * scaling + property string specificFolderMonitorName: "" + NHeader { label: "Wallpaper settings" description: "Control how wallpapers are managed and displayed." @@ -18,7 +20,7 @@ ColumnLayout { NToggle { label: "Enable wallpaper management" - description: "Manage wallpapers with Noctalia. (Uncheck if you prefer using another application)." + description: "Manage wallpapers with Noctalia. Uncheck if you prefer using another application." checked: Settings.data.wallpaper.enabled onToggled: checked => Settings.data.wallpaper.enabled = checked Layout.bottomMargin: Style.marginL * scaling @@ -29,7 +31,7 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NInputButton { + NTextInputButton { id: wallpaperPathInput label: "Wallpaper folder" description: "Path to your main wallpaper folder." @@ -37,13 +39,8 @@ ColumnLayout { buttonIcon: "folder-open" buttonTooltip: "Browse for wallpaper folder" Layout.fillWidth: true - - onInputEditingFinished: { - Settings.data.wallpaper.directory = text - } - onButtonClicked: { - openFileManager() - } + onInputEditingFinished: Settings.data.wallpaper.directory = text + onButtonClicked: mainFolderPicker.open() } // Monitor-specific directories @@ -83,17 +80,15 @@ ColumnLayout { font.pointSize: Style.fontSizeM * scaling } - NInputButton { + NTextInputButton { text: WallpaperService.getMonitorDirectory(modelData.name) buttonIcon: "folder-open" buttonTooltip: "Browse for wallpaper folder" Layout.fillWidth: true - - onInputEditingFinished: { - WallpaperService.setMonitorDirectory(modelData.name, text) - } + onInputEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, text) onButtonClicked: { - openMonitorFileManager(modelData.name) + specificFolderMonitorName = modelData.name + monitorFolderPicker.open() } } } @@ -343,26 +338,17 @@ ColumnLayout { Layout.bottomMargin: Style.marginXL * scaling } - // File manager functions - function openFileManager() { - FilePickerService.open({ - "title": "Select Wallpaper Folder", - "initialPath": Settings.data.wallpaper.directory || Quickshell.env("HOME"), - "selectFiles": false, - "scaling": scaling, - "parent": root, - "onSelected": path => Settings.data.wallpaper.directory = path - }) + NFilePicker { + id: mainFolderPicker + pickerType: "folder" + title: "Select wallpaper folder" + onAccepted: paths => Settings.data.wallpaper.directory = paths[0] } - function openMonitorFileManager(monitorName) { - FilePickerService.open({ - "title": "Select Monitor Wallpaper Folder", - "initialPath": WallpaperService.getMonitorDirectory(monitorName), - "selectFiles": false, - "scaling": scaling, - "parent": root, - "onSelected": path => WallpaperService.setMonitorDirectory(monitorName, path) - }) + NFilePicker { + id: monitorFolderPicker + pickerType: "folder" + title: "Select monitor wallpaper folder" + onAccepted: paths => WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0]) } } diff --git a/Modules/WallpaperSelector/WallpaperSelector.qml b/Modules/Wallpaper/WallpaperPanel.qml similarity index 99% rename from Modules/WallpaperSelector/WallpaperSelector.qml rename to Modules/Wallpaper/WallpaperPanel.qml index 0781c11b..7c0bad58 100644 --- a/Modules/WallpaperSelector/WallpaperSelector.qml +++ b/Modules/Wallpaper/WallpaperPanel.qml @@ -18,7 +18,8 @@ NPanel { panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true panelKeyboardFocus: true - draggable: true + + draggable: !PanelService.hasOpenedPopup panelContent: Rectangle { id: wallpaperPanel diff --git a/Services/BarService.qml b/Services/BarService.qml index 9fc6749b..a459dee0 100644 --- a/Services/BarService.qml +++ b/Services/BarService.qml @@ -1,11 +1,32 @@ pragma Singleton +import QtQuick import Quickshell import qs.Commons Singleton { id: root + property bool hasAudioVisualizer: false + + // Simple timer that run once when the widget structure has changed + // and determine if any MediaMini widget has the visualizer on + Timer { + id: timerCheckVisualizer + interval: 100 + repeat: false + onTriggered: { + hasAudioVisualizer = false + const widgets = getAllWidgetInstances("MediaMini") + for (var i = 0; i < widgets.length; i++) { + const widget = widgets[i] + if (widget.showVisualizer) { + hasAudioVisualizer = true + } + } + } + } + // Registry to store actual widget instances // Key format: "screenName|section|widgetId|index" property var widgetInstances: ({}) @@ -20,6 +41,9 @@ Singleton { "index": index, "instance": instance } + + timerCheckVisualizer.restart() + Logger.log("BarService", "Registered widget:", key) } diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index d4594468..913286a6 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -15,8 +15,9 @@ Singleton { "Bluetooth": bluetoothComponent, "Brightness": brightnessComponent, "Clock": clockComponent, + "ControlCenter": controlCenterComponent, "CustomButton": customButtonComponent, - "DarkModeToggle": darkModeToggle, + "DarkMode": darkMode, "KeepAwake": keepAwakeComponent, "KeyboardLayout": keyboardLayoutComponent, "MediaMini": mediaMiniComponent, @@ -24,9 +25,8 @@ Singleton { "NightLight": nightLightComponent, "NotificationHistory": notificationHistoryComponent, "PowerProfile": powerProfileComponent, - "PowerToggle": powerToggleComponent, - "ScreenRecorderIndicator": screenRecorderIndicatorComponent, - "SidePanelToggle": sidePanelToggleComponent, + "ScreenRecorder": screenRecorderComponent, + "SessionMenu": sessionMenuComponent, "Spacer": spacerComponent, "SystemMonitor": systemMonitorComponent, "Taskbar": taskbarComponent, @@ -100,7 +100,7 @@ Singleton { "showVisualizer": false, "visualizerType": "linear" }, - "SidePanelToggle": { + "ControlCenter": { "allowUserSettings": true, "useDistroLogo": false, "icon": "noctalia", @@ -135,8 +135,8 @@ Singleton { property Component customButtonComponent: Component { CustomButton {} } - property Component darkModeToggle: Component { - DarkModeToggle {} + property Component darkMode: Component { + DarkMode {} } property Component keyboardLayoutComponent: Component { KeyboardLayout {} @@ -159,14 +159,14 @@ Singleton { property Component powerProfileComponent: Component { PowerProfile {} } - property Component powerToggleComponent: Component { - PowerToggle {} + property Component sessionMenuComponent: Component { + SessionMenu {} } - property Component screenRecorderIndicatorComponent: Component { - ScreenRecorderIndicator {} + property Component screenRecorderComponent: Component { + ScreenRecorder {} } - property Component sidePanelToggleComponent: Component { - SidePanelToggle {} + property Component controlCenterComponent: Component { + ControlCenter {} } property Component spacerComponent: Component { Spacer {} diff --git a/Services/CavaService.qml b/Services/CavaService.qml index d31d3e92..07b8de7f 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -8,9 +8,9 @@ import qs.Commons Singleton { id: root + property bool shouldRun: BarService.hasAudioVisualizer || (PanelService.getPanel("controlCenterPanel") === PanelService.openedPanel) || PanelService.lockScreen.active property var values: Array(barsCount).fill(0) property int barsCount: 24 - property var config: ({ "general": { "bars": barsCount, @@ -37,8 +37,11 @@ Singleton { Process { id: process stdinEnabled: true - running: MediaService.isPlaying + running: root.shouldRun command: ["cava", "-p", "/dev/stdin"] + onRunningChanged: { + Logger.log("Cava", "Process running:", running) + } onExited: { stdinEnabled = true values = Array(barsCount).fill(0) diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 8fc7c935..d2064350 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -114,9 +114,9 @@ Singleton { // Get window title for focused window function getFocusedWindowTitle() { if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { - return windows[focusedWindowIndex].title || "(Unnamed window)" + return windows[focusedWindowIndex].title || "" } - return "(No active window)" + return "" } // Generic workspace switching diff --git a/Services/FilePickerService.qml b/Services/FilePickerService.qml deleted file mode 100644 index 94ff4ede..00000000 --- a/Services/FilePickerService.qml +++ /dev/null @@ -1,47 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell - -QtObject { - id: root - - // Function to open a file manager dialog - function open(options) { - var component = Qt.createComponent(Qt.resolvedUrl(Quickshell.shellDir + "/Widgets/NFilePicker.qml")) - if (component.status === Component.Ready) { - // Extract directory from file path if it's a file - var initialPath = options.initialPath || Quickshell.env("HOME") - if (options.selectFiles && initialPath !== Quickshell.env("HOME")) { - // If selecting files and path is not home, extract directory - var pathParts = initialPath.split('/') - if (pathParts.length > 1) { - pathParts.pop() // Remove filename - initialPath = pathParts.join('/') || '/' - } - } - - var dialog = component.createObject(options.parent || Overlay.overlay, { - "title": options.title || "Select File/Folder", - "initialPath": initialPath, - "selectFiles": options.selectFiles || false, - "selectFolders": !options.selectFiles || false, - "scaling": options.scaling || 1.0 - }) - if (dialog) { - if (options.onSelected) { - if (options.selectFiles) { - dialog.fileSelected.connect(options.onSelected) - } else { - dialog.folderSelected.connect(options.onSelected) - } - } - dialog.open() - return dialog - } - } else { - console.error("Component error:", component.errorString()) - } - return null - } -} diff --git a/Modules/IPC/IPCManager.qml b/Services/IPCService.qml similarity index 77% rename from Modules/IPC/IPCManager.qml rename to Services/IPCService.qml index ba644c75..5031bbbc 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Services/IPCService.qml @@ -109,18 +109,35 @@ Item { } } + // TODO: delete in next major release IpcHandler { target: "powerPanel" function toggle() { - powerPanel.toggle() + sessionMenuPanel.toggle() + ToastService.showWarning("IPC", "PowerPanel has been renamed to SessionMenu, this IPC call will be deprecated soon. Please use \"ipc call sessionMenu toggle\" instead.", 8000) + } + } + IpcHandler { + target: "sessionMenu" + function toggle() { + sessionMenuPanel.toggle() } } + // TODO: delete in next major release IpcHandler { target: "sidePanel" function toggle() { // Will attempt to open the panel next to the bar button if any. - sidePanel.toggle(BarService.lookupWidget("SidePanelToggle")) + controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter")) + ToastService.showWarning("IPC", "SidePanel has been renamed to ControlCenter, this IPC call will be deprecated soon. Please use \"ipc call controlCenter toggle\" instead.", 8000) + } + } + IpcHandler { + target: "controlCenter" + function toggle() { + // Will attempt to open the panel next to the bar button if any. + controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter")) } } @@ -129,7 +146,7 @@ Item { target: "wallpaper" function toggle() { if (Settings.data.wallpaper.enabled) { - wallpaperSelector.toggle() + wallpaperPanel.toggle() } } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index a45ee385..7bbdcc60 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -320,7 +320,7 @@ Singleton { return "" if (icon.startsWith("/") || icon.startsWith("file://")) return icon - return AppIcons.iconFromName(icon) + return ThemeIcons.iconFromName(icon) } function stripTags(text) { diff --git a/Services/PanelService.qml b/Services/PanelService.qml index f1df973f..5d33aeb7 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -10,15 +10,19 @@ Singleton { // This is not a panel... property var lockScreen: null - // Currently opened panel - property var openedPanel: null - readonly property bool hasOpenedPanel: (openedPanel !== null) - + // Panels property var registeredPanels: ({}) - + property var openedPanel: null + property bool hasOpenedPanel: false signal willOpen signal willClose + // Currently opened popups, can have more than one. + // ex: when opening an NIconPicker from a widget setting. + property var openedPopups: [] + property bool hasOpenedPopup: false + signal popupChanged + // Register this panel function registerPanel(panel) { registeredPanels[panel.objectName] = panel @@ -41,12 +45,15 @@ Singleton { openedPanel.close() } openedPanel = panel + hasOpenedPanel = true // emit signal willOpen() } function willClosePanel(panel) { + hasOpenedPanel = false + // emit signal willClose() } @@ -56,4 +63,17 @@ Singleton { openedPanel = null } } + + // Popups + function willOpenPopup(popup) { + openedPopups.push(popup) + hasOpenedPopup = (openedPopups.length !== 0) + popupChanged() + } + + function willClosePopup(popup) { + openedPopups = openedPopups.filter(p => p !== popup) + hasOpenedPopup = (openedPopups.length !== 0) + popupChanged() + } } diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index 8a5512bd..e45a3785 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -12,6 +12,8 @@ Singleton { readonly property var settings: Settings.data.screenRecorder property bool isRecording: false property bool isPending: false + // True only if the recorder actually started capturing at least once + property bool hasActiveRecording: false property string outputPath: "" property bool isAvailable: ProgramCheckerService.gpuScreenRecorderAvailable @@ -36,7 +38,16 @@ Singleton { return } isPending = true + hasActiveRecording = false + // First, ensure xdg-desktop-portal and a compositor portal are running + portalCheckProcess.exec({ + "command": ["sh", "-c", // require core portal AND one of the backends + "pidof xdg-desktop-portal >/dev/null 2>&1 && (pidof xdg-desktop-portal-wlr >/dev/null 2>&1 || pidof xdg-desktop-portal-hyprland >/dev/null 2>&1 || pidof xdg-desktop-portal-gnome >/dev/null 2>&1 || pidof xdg-desktop-portal-kde >/dev/null 2>&1)"] + }) + } + + function launchRecorder() { var filename = Time.getFormattedTimestamp() + ".mp4" var videoDir = settings.directory if (videoDir && !videoDir.endsWith("/")) { @@ -56,7 +67,7 @@ Singleton { notify-send "gpu-screen-recorder not installed!" -u critical fi` - // Use Process instead of execDetached so we can monitor it + // Use Process instead of execDetached so we can monitor it and read stderr recorderProcess.exec({ "command": ["sh", "-c", command] }) @@ -71,12 +82,15 @@ Singleton { return } + ToastService.showNotice("Stopping recording…", outputPath, 2000) + Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder' || pkill -SIGINT -f 'com.dec05eba.gpu_screen_recorder'"]) isRecording = false isPending = false pendingTimer.running = false monitorTimer.running = false + hasActiveRecording = false // Just in case, force kill after 3 seconds killTimer.running = true @@ -85,15 +99,50 @@ Singleton { // Process to run and monitor gpu-screen-recorder Process { id: recorderProcess + stdout: StdioCollector {} + stderr: StdioCollector {} onExited: function (exitCode, exitStatus) { if (isPending) { // Process ended while we were pending - likely cancelled or error isPending = false pendingTimer.running = false + // If it failed to start, show a clear error toast with stderr + if (exitCode !== 0) { + const err = String(stderr.text || "").trim() + if (err.length > 0) + ToastService.showError("Failed to start recording", err, 7000) + else + ToastService.showError("Failed to start recording", "gpu-screen-recorder exited unexpectedly.", 7000) + } } else if (isRecording) { // Process ended normally while recording isRecording = false monitorTimer.running = false + // Consider successful save if exitCode == 0 + if (exitCode === 0) { + ToastService.showNotice("Recording saved", outputPath, 5000) + } else { + const err2 = String(stderr.text || "").trim() + if (err2.length > 0) + ToastService.showError("Recording failed", err2, 7000) + else + ToastService.showError("Recording failed", "The recorder exited with an error.", 7000) + } + } + } + } + + // Pre-flight check for xdg-desktop-portal + Process { + id: portalCheckProcess + onExited: function (exitCode, exitStatus) { + if (exitCode === 0) { + // Portals available, proceed to launch + launchRecorder() + } else { + isPending = false + hasActiveRecording = false + ToastService.showError("Desktop portals not running", "Start xdg-desktop-portal and a compositor portal (wlr/hyprland/gnome/kde).", 8000) } } } @@ -108,7 +157,10 @@ Singleton { // Process is still running after 2 seconds - assume recording started successfully isPending = false isRecording = true + hasActiveRecording = true monitorTimer.running = true + // Don't show a toast when recording starts to avoid having the toast in every video. + //ToastService.showNotice("Recording started", outputPath, 4000) } else if (isPending) { // Process not running anymore - was cancelled or failed isPending = false diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index 7dc8841c..e80b6b31 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -13,7 +13,6 @@ Rectangle { property color backgroundColor: Color.mPrimary property color textColor: Color.mOnPrimary property color hoverColor: Color.mTertiary - property color pressColor: Color.mSecondary property bool enabled: true property real fontSize: Style.fontSizeM * scaling property int fontWeight: Style.fontWeightBold @@ -38,8 +37,6 @@ Rectangle { color: { if (!enabled) return outlined ? Color.transparent : Qt.lighter(Color.mSurfaceVariant, 1.2) - if (pressed) - return pressColor if (hovered) return hoverColor return outlined ? Color.transparent : backgroundColor @@ -113,7 +110,7 @@ Rectangle { if (!root.enabled) return Color.mOnSurfaceVariant if (root.outlined) { - if (root.pressed || root.hovered) + if (root.hovered) return root.textColor return root.backgroundColor } @@ -153,33 +150,24 @@ Rectangle { } onExited: { root.hovered = false - root.pressed = false if (tooltipText) { tooltip.hide() } } onPressed: mouse => { - root.pressed = true + if (tooltipText) { + tooltip.hide() + } + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button == Qt.RightButton) { + root.rightClicked() + } else if (mouse.button == Qt.MiddleButton) { + root.middleClicked() + } } - onReleased: mouse => { - root.pressed = false - if (tooltipText) { - tooltip.hide() - } - if (!root.hovered) { - return - } - if (mouse.button === Qt.LeftButton) { - root.clicked() - } else if (mouse.button == Qt.RightButton) { - root.rightClicked() - } else if (mouse.button == Qt.MiddleButton) { - root.middleClicked - } - } onCanceled: { - root.pressed = false root.hovered = false if (tooltipText) { tooltip.hide() diff --git a/Widgets/NColorPickerDialog.qml b/Widgets/NColorPickerDialog.qml index 6cf6965f..836fb4ab 100644 --- a/Widgets/NColorPickerDialog.qml +++ b/Widgets/NColorPickerDialog.qml @@ -26,7 +26,14 @@ Popup { y: (parent.height - height) * 0.5 modal: true - clip: true + + onOpened: { + PanelService.willOpenPopup(root) + } + + onClosed: { + PanelService.willClosePopup(root) + } function rgbToHsv(r, g, b) { r /= 255 @@ -110,9 +117,9 @@ Popup { border.width: Math.max(1, Style.borderM * scaling) } - NScrollView { + contentItem: NScrollView { id: scrollView - anchors.fill: parent + width: parent.width verticalPolicy: ScrollBar.AlwaysOff horizontalPolicy: ScrollBar.AlwaysOff diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 9c9b5bd2..635876a0 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -85,16 +85,23 @@ RowLayout { popup: Popup { y: combo.height - width: combo.width + implicitWidth: combo.width - Style.marginM * scaling implicitHeight: Math.min(root.popupHeight, contentItem.implicitHeight + Style.marginM * scaling * 2) padding: Style.marginM * scaling - contentItem: ListView { - property var comboBoxRoot: root - clip: true - implicitHeight: contentHeight + onOpened: { + PanelService.willOpenPopup(root) + } + + onClosed: { + PanelService.willClosePopup(root) + } + + contentItem: NListView { model: combo.popup.visible ? root.model : null - ScrollIndicator.vertical: ScrollIndicator {} + implicitHeight: contentHeight + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded delegate: ItemDelegate { width: combo.width @@ -108,17 +115,15 @@ RowLayout { } onClicked: { - ListView.view.comboBoxRoot.selected(ListView.view.comboBoxRoot.model.get(index).key) + root.selected(root.model.get(index).key) combo.currentIndex = index combo.popup.close() } - contentItem: NText { - text: name - font.pointSize: Style.fontSizeM * scaling - color: highlighted ? Color.mSurface : Color.mOnSurface - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight + background: Rectangle { + width: combo.width - Style.marginM * scaling * 3 + color: highlighted ? Color.mTertiary : Color.transparent + radius: Style.radiusS * scaling Behavior on color { ColorAnimation { duration: Style.animationFast @@ -126,10 +131,12 @@ RowLayout { } } - background: Rectangle { - width: combo.width - Style.marginM * scaling * 3 - color: highlighted ? Color.mTertiary : Color.transparent - radius: Style.radiusS * scaling + contentItem: NText { + text: name + font.pointSize: Style.fontSizeM * scaling + color: highlighted ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight Behavior on color { ColorAnimation { duration: Style.animationFast diff --git a/Widgets/NContextMenu.qml b/Widgets/NContextMenu.qml new file mode 100644 index 00000000..a1b7562c --- /dev/null +++ b/Widgets/NContextMenu.qml @@ -0,0 +1,113 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services + +Popup { + id: root + + property alias model: listView.model + property real itemHeight: 36 * scaling + property real itemPadding: Style.marginM * scaling + + signal triggered(string action) + + width: 180 * scaling + padding: Style.marginS * scaling + + onOpened: PanelService.willOpenPopup(root) + onClosed: PanelService.willClosePopup(root) + + background: Rectangle { + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusM * scaling + } + + contentItem: ListView { + id: listView + implicitHeight: contentHeight + spacing: Style.marginXXS * scaling + interactive: contentHeight > root.height + clip: true + + delegate: ItemDelegate { + id: menuItem + width: listView.width + height: modelData.visible !== false ? root.itemHeight : 0 + visible: modelData.visible !== false + opacity: modelData.enabled !== false ? 1.0 : 0.5 + enabled: modelData.enabled !== false + + // Store reference to the popup + property var popup: root + + background: Rectangle { + color: menuItem.hovered && menuItem.enabled ? Color.mTertiary : Color.transparent + radius: Style.radiusS * scaling + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + contentItem: RowLayout { + spacing: Style.marginS * scaling + + // Optional icon + NIcon { + visible: modelData.icon !== undefined + icon: modelData.icon || "" + font.pointSize: Style.fontSizeM * scaling + color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface + Layout.leftMargin: root.itemPadding + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + NText { + text: modelData.label || modelData.text || "" + font.pointSize: Style.fontSizeM * scaling + color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + Layout.fillWidth: true + Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + onClicked: { + if (enabled) { + popup.triggered(modelData.action || modelData.key || index.toString()) + popup.close() + } + } + } + } + + // Helper function to open at mouse position + function openAt(x, y) { + root.x = x + root.y = y + root.open() + } + + // Helper function to open at item + function openAtItem(item, mouseX, mouseY) { + var pos = item.mapToItem(root.parent, mouseX || 0, mouseY || 0) + openAt(pos.x, pos.y) + } +} diff --git a/Widgets/NDateTimeTokens.qml b/Widgets/NDateTimeTokens.qml index b4897390..904f3c29 100644 --- a/Widgets/NDateTimeTokens.qml +++ b/Widgets/NDateTimeTokens.qml @@ -21,335 +21,312 @@ Rectangle { anchors.margins: Style.marginS * scaling spacing: Style.marginS * scaling - // Scrollable list of tokens - NScrollView { + NListView { + id: tokensList Layout.fillWidth: true Layout.fillHeight: true - clip: true - horizontalPolicy: ScrollBar.AlwaysOff - verticalPolicy: ScrollBar.AsNeeded + model: ListModel { - ListView { - id: tokensList - model: ListModel { + // Common format combinations + ListElement { + category: "Common" + token: "h:mm AP" + description: "12-hour time with minutes" + example: "2:30 PM" + } + ListElement { + category: "Common" + token: "HH:mm" + description: "24-hour time with minutes" + example: "14:30" + } + ListElement { + category: "Common" + token: "HH:mm:ss" + description: "24-hour time with seconds" + example: "14:30:45" + } + ListElement { + category: "Common" + token: "ddd MMM d" + description: "Weekday, month and day" + example: "Mon Dec 25" + } + ListElement { + category: "Common" + token: "yyyy-MM-dd" + description: "ISO date format" + example: "2023-12-25" + } + ListElement { + category: "Common" + token: "MM/dd/yyyy" + description: "US date format" + example: "12/25/2023" + } + ListElement { + category: "Common" + token: "dd.MM.yyyy" + description: "European date format" + example: "25.12.2023" + } + ListElement { + category: "Common" + token: "ddd, MMM dd" + description: "Weekday with date" + example: "Fri, Dec 12" + } - // Common format combinations - ListElement { - category: "Common" - token: "h:mm AP" - description: "12-hour time with minutes" - example: "2:30 PM" - } - ListElement { - category: "Common" - token: "HH:mm" - description: "24-hour time with minutes" - example: "14:30" - } - ListElement { - category: "Common" - token: "HH:mm:ss" - description: "24-hour time with seconds" - example: "14:30:45" - } - ListElement { - category: "Common" - token: "ddd MMM d" - description: "Weekday, month and day" - example: "Mon Dec 25" - } - ListElement { - category: "Common" - token: "yyyy-MM-dd" - description: "ISO date format" - example: "2023-12-25" - } - ListElement { - category: "Common" - token: "MM/dd/yyyy" - description: "US date format" - example: "12/25/2023" - } - ListElement { - category: "Common" - token: "dd.MM.yyyy" - description: "European date format" - example: "25.12.2023" - } - ListElement { - category: "Common" - token: "ddd, MMM dd" - description: "Weekday with date" - example: "Fri, Dec 12" - } + // Hour tokens + // ListElement { + // category: "Hour" + // token: "h" + // description: "Hour without leading zero (12-hour when used with AP/ap, otherwise 24-hour)" + // example: "2 (needs AP/ap for 12hr)" + // } + // ListElement { + // category: "Hour" + // token: "hh" + // description: "Hour with leading zero (12-hour when used with AP/ap, otherwise 24-hour)" + // example: "02 (needs AP/ap for 12hr)" + // } + // ListElement { + // category: "Hour" + // token: "h AP" + // description: "12-hour format with AM/PM" + // example: "2 PM" + // } + // ListElement { + // category: "Hour" + // token: "hh AP" + // description: "12-hour format with leading zero and AM/PM" + // example: "02 PM" + // } + ListElement { + category: "Hour" + token: "H" + description: "Hour without leading zero (0-23) - 24-hour format" + example: "14" + } + ListElement { + category: "Hour" + token: "HH" + description: "Hour with leading zero (00-23) - 24-hour format" + example: "14" + } - // Hour tokens - // ListElement { - // category: "Hour" - // token: "h" - // description: "Hour without leading zero (12-hour when used with AP/ap, otherwise 24-hour)" - // example: "2 (needs AP/ap for 12hr)" - // } - // ListElement { - // category: "Hour" - // token: "hh" - // description: "Hour with leading zero (12-hour when used with AP/ap, otherwise 24-hour)" - // example: "02 (needs AP/ap for 12hr)" - // } - // ListElement { - // category: "Hour" - // token: "h AP" - // description: "12-hour format with AM/PM" - // example: "2 PM" - // } - // ListElement { - // category: "Hour" - // token: "hh AP" - // description: "12-hour format with leading zero and AM/PM" - // example: "02 PM" - // } - ListElement { - category: "Hour" - token: "H" - description: "Hour without leading zero (0-23) - 24-hour format" - example: "14" - } - ListElement { - category: "Hour" - token: "HH" - description: "Hour with leading zero (00-23) - 24-hour format" - example: "14" - } + // Minute tokens + ListElement { + category: "Minute" + token: "m" + description: "Minute without leading zero (0-59)" + example: "30" + } + ListElement { + category: "Minute" + token: "mm" + description: "Minute with leading zero (00-59)" + example: "30" + } - // Minute tokens - ListElement { - category: "Minute" - token: "m" - description: "Minute without leading zero (0-59)" - example: "30" - } - ListElement { - category: "Minute" - token: "mm" - description: "Minute with leading zero (00-59)" - example: "30" - } + // Second tokens + ListElement { + category: "Second" + token: "s" + description: "Second without leading zero (0-59)" + example: "45" + } + ListElement { + category: "Second" + token: "ss" + description: "Second with leading zero (00-59)" + example: "45" + } - // Second tokens - ListElement { - category: "Second" - token: "s" - description: "Second without leading zero (0-59)" - example: "45" - } - ListElement { - category: "Second" - token: "ss" - description: "Second with leading zero (00-59)" - example: "45" - } + // AM/PM tokens + ListElement { + category: "AM/PM" + token: "AP" + description: "AM/PM in uppercase" + example: "PM" + } + ListElement { + category: "AM/PM" + token: "ap" + description: "am/pm in lowercase" + example: "pm" + } - // AM/PM tokens - ListElement { - category: "AM/PM" - token: "AP" - description: "AM/PM in uppercase" - example: "PM" - } - ListElement { - category: "AM/PM" - token: "ap" - description: "am/pm in lowercase" - example: "pm" - } + // Timezone tokens + ListElement { + category: "Timezone" + token: "t" + description: "Timezone abbreviation" + example: "UTC" + } - // Timezone tokens - ListElement { - category: "Timezone" - token: "t" - description: "Timezone abbreviation" - example: "UTC" - } + // Year tokens + ListElement { + category: "Year" + token: "yy" + description: "Year as two-digit number (00-99)" + example: "23" + } + ListElement { + category: "Year" + token: "yyyy" + description: "Year as four-digit number" + example: "2023" + } - // Year tokens - ListElement { - category: "Year" - token: "yy" - description: "Year as two-digit number (00-99)" - example: "23" - } - ListElement { - category: "Year" - token: "yyyy" - description: "Year as four-digit number" - example: "2023" - } + // Month tokens + ListElement { + category: "Month" + token: "M" + description: "Month as number without leading zero (1-12)" + example: "12" + } + ListElement { + category: "Month" + token: "MM" + description: "Month as number with leading zero (01-12)" + example: "12" + } + ListElement { + category: "Month" + token: "MMM" + description: "Abbreviated month name" + example: "Dec" + } + ListElement { + category: "Month" + token: "MMMM" + description: "Full month name" + example: "December" + } - // Month tokens - ListElement { - category: "Month" - token: "M" - description: "Month as number without leading zero (1-12)" - example: "12" - } - ListElement { - category: "Month" - token: "MM" - description: "Month as number with leading zero (01-12)" - example: "12" - } - ListElement { - category: "Month" - token: "MMM" - description: "Abbreviated month name" - example: "Dec" - } - ListElement { - category: "Month" - token: "MMMM" - description: "Full month name" - example: "December" - } + // Day tokens + ListElement { + category: "Day" + token: "d" + description: "Day without leading zero (1-31)" + example: "25" + } + ListElement { + category: "Day" + token: "dd" + description: "Day with leading zero (01-31)" + example: "25" + } + ListElement { + category: "Day" + token: "ddd" + description: "Abbreviated day name" + example: "Mon" + } + ListElement { + category: "Day" + token: "dddd" + description: "Full day name" + example: "Monday" + } + } - // Day tokens - ListElement { - category: "Day" - token: "d" - description: "Day without leading zero (1-31)" - example: "25" + delegate: Rectangle { + id: tokenDelegate + width: tokensList.width + height: layout.implicitHeight + Style.marginS * scaling + radius: Style.radiusS * scaling + color: { + if (tokenMouseArea.containsMouse) { + return Qt.alpha(Color.mPrimary, 0.1) } - ListElement { - category: "Day" - token: "dd" - description: "Day with leading zero (01-31)" - example: "25" - } - ListElement { - category: "Day" - token: "ddd" - description: "Abbreviated day name" - example: "Mon" - } - ListElement { - category: "Day" - token: "dddd" - description: "Full day name" - example: "Monday" + return index % 2 === 0 ? Color.mSurfaceVariant : Qt.alpha(Color.mSurfaceVariant, 0.6) + } + + // Mouse area for the entire delegate + MouseArea { + id: tokenMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + // Emit the signal with the token + root.tokenClicked(model.token) + + // Visual feedback + clickAnimation.start() } } - delegate: Rectangle { - id: tokenDelegate - width: tokensList.width - height: layout.implicitHeight + Style.marginS * scaling - radius: Style.radiusS * scaling - color: { - if (tokenMouseArea.containsMouse) { - return Qt.alpha(Color.mPrimary, 0.1) - } - return index % 2 === 0 ? Color.mSurfaceVariant : Qt.alpha(Color.mSurfaceVariant, 0.6) + // Click animation + SequentialAnimation { + id: clickAnimation + PropertyAnimation { + target: tokenDelegate + property: "color" + to: Qt.alpha(Color.mPrimary, 0.3) + duration: 100 } - - // Mouse area for the entire delegate - MouseArea { - id: tokenMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - // Emit the signal with the token - root.tokenClicked(model.token) - - // Visual feedback - clickAnimation.start() - } + PropertyAnimation { + target: tokenDelegate + property: "color" + to: tokenMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) : (index % 2 === 0 ? Color.mSurface : Color.mSurfaceVariant) + duration: 200 } + } - // Click animation - SequentialAnimation { - id: clickAnimation - PropertyAnimation { - target: tokenDelegate - property: "color" - to: Qt.alpha(Color.mPrimary, 0.3) - duration: 100 - } - PropertyAnimation { - target: tokenDelegate - property: "color" - to: tokenMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) : (index % 2 === 0 ? Color.mSurface : Color.mSurfaceVariant) - duration: 200 - } - } + RowLayout { + id: layout + anchors.fill: parent + anchors.margins: Style.marginXS * scaling + spacing: Style.marginM * scaling - RowLayout { - id: layout - anchors.fill: parent - anchors.margins: Style.marginXS * scaling - spacing: Style.marginM * scaling + // Category badge + Rectangle { + Layout.alignment: Qt.AlignVCenter + width: 70 * scaling + height: 22 * scaling + color: getCategoryColor(model.category)[0] + radius: Style.radiusS * scaling + opacity: tokenMouseArea.containsMouse ? 0.9 : 1.0 - // Category badge - Rectangle { - Layout.alignment: Qt.AlignVCenter - width: 70 * scaling - height: 22 * scaling - color: getCategoryColor(model.category)[0] - radius: Style.radiusS * scaling - opacity: tokenMouseArea.containsMouse ? 0.9 : 1.0 - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - } - } - - NText { - anchors.centerIn: parent - text: model.category - color: getCategoryColor(model.category)[1] - font.pointSize: Style.fontSizeXS * scaling + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast } } - // Token - Made more prominent and clickable - Rectangle { - id: tokenButton - Layout.alignment: Qt.AlignVCenter // Added this line - width: 100 * scaling - height: 22 * scaling - color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface - radius: Style.radiusS * scaling - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - - NText { - anchors.centerIn: parent - text: model.token - color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurface - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightBold - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - } - } - - // Description NText { - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter // Added this line - text: model.description - color: tokenMouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurfaceVariant + anchors.centerIn: parent + text: model.category + color: getCategoryColor(model.category)[1] + font.pointSize: Style.fontSizeXS * scaling + } + } + + // Token - Made more prominent and clickable + Rectangle { + id: tokenButton + Layout.alignment: Qt.AlignVCenter // Added this line + width: 100 * scaling + height: 22 * scaling + color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface + radius: Style.radiusS * scaling + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: model.token + color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurface font.pointSize: Style.fontSizeS * scaling - wrapMode: Text.WordWrap + font.weight: Style.fontWeightBold Behavior on color { ColorAnimation { @@ -357,41 +334,57 @@ Rectangle { } } } + } - // Live example - Rectangle { - Layout.alignment: Qt.AlignVCenter // Added this line - width: 90 * scaling - height: 22 * scaling - color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant - radius: Style.radiusS * scaling - border.color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) + // Description + NText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter // Added this line + text: model.description + color: tokenMouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeS * scaling + wrapMode: Text.WordWrap + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + // Live example + Rectangle { + Layout.alignment: Qt.AlignVCenter // Added this line + width: 90 * scaling + height: 22 * scaling + color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant + radius: Style.radiusS * scaling + border.color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: Qt.formatDateTime(root.sampleDate, model.token) + color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurfaceVariant + font.pointSize: Style.fontSizeS * scaling Behavior on color { ColorAnimation { duration: Style.animationFast } } - - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast - } - } - - NText { - anchors.centerIn: parent - text: Qt.formatDateTime(root.sampleDate, model.token) - color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurfaceVariant - font.pointSize: Style.fontSizeS * scaling - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - } } } } diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml index 39459404..f5fb2336 100644 --- a/Widgets/NFilePicker.qml +++ b/Widgets/NFilePicker.qml @@ -1,816 +1,205 @@ +import QtCore import QtQuick -import QtQuick.Layouts +import QtQuick.Dialogs import QtQuick.Controls -import Qt.labs.folderlistmodel -import Quickshell -import Quickshell.Io -import qs.Commons import qs.Services -import qs.Widgets -import "../Helpers/FuzzySort.js" as FuzzySort -Popup { +Item { id: root - // Properties - property string title: "File Picker" - property string initialPath: Quickshell.env("HOME") || "/home" - property bool selectFiles: true - property bool selectFolders: true - property var nameFilters: ["*"] - property bool showDirs: true - property real scaling: 1.0 + // Public API Properties + property string initialPath: "" property var selectedPaths: [] - property string currentPath: initialPath - property bool shouldResetSelection: false + property string selectedPath: "" + property bool multipleSelection: false + property string pickerType: "file" // "file" or "folder" + property var nameFilters: ["All files (*)"] // e.g., ["Image files (*.png *.jpg)", "Text files (*.txt)"] + property string title: pickerType === "folder" ? "Select Folder" : "Select File" + property string acceptLabel: "Select" + property string rejectLabel: "Cancel" - // Signals - signal fileSelected(string path) - signal filesSelected(var paths) - signal folderSelected(string path) - signal cancelled + // State properties + property bool isOpen: false - function openFileManager() { - if (!root.currentPath) - root.currentPath = root.initialPath - shouldResetSelection = true - open() - } + // Signals for external connections + signal accepted(var paths) + signal rejected + signal pathSelected(string path) + signal pathsSelected(var paths) + signal beforeOpen + signal afterClose - function getFileIcon(fileName) { - const ext = fileName.split('.').pop().toLowerCase() - const iconMap = { - "txt": 'filepicker-file-text', - "md": 'filepicker-file-text', - "log": 'filepicker-file-text', - "jpg": 'filepicker-photo', - "jpeg": 'filepicker-photo', - "png": 'filepicker-photo', - "gif": 'filepicker-photo', - "bmp": 'filepicker-photo', - "svg": 'filepicker-photo', - "mp4": 'filepicker-video', - "avi": 'filepicker-video', - "mkv": 'filepicker-video', - "mov": 'filepicker-video', - "mp3": 'filepicker-music', - "wav": 'filepicker-music', - "flac": 'filepicker-music', - "ogg": 'filepicker-music', - "zip": 'filepicker-archive', - "tar": 'filepicker-archive', - "gz": 'filepicker-archive', - "rar": 'filepicker-archive', - "7z": 'filepicker-archive', - "pdf": 'filepicker-text', - "doc": 'filepicker-text', - "docx": 'filepicker-text', - "xls": 'filepicker-table', - "xlsx": 'filepicker-table', - "ppt": 'filepicker-presentation', - "pptx": 'filepicker-presentation', - "html": 'filepicker-code', - "htm": 'filepicker-code', - "css": 'filepicker-code', - "js": 'filepicker-code', - "json": 'filepicker-code', - "xml": 'filepicker-code', - "exe": 'filepicker-settings', - "app": 'filepicker-settings', - "deb": 'filepicker-settings', - "rpm": 'filepicker-settings' + // Public functions + function open() { + beforeOpen() + + if (PanelService.openedPanel !== null) { + PanelService.openedPanel.isMasked = true } - return iconMap[ext] || 'filepicker-file' + + for (var i = 0; i < PanelService.openedPopups.length; i++) { + PanelService.openedPopups[i].isMasked = true + } + + isOpen = true + + // Small delay to ensure panel changes happen first + Qt.callLater(function () { + if (pickerType === "folder") { + folderDialog.open() + } else { + fileDialog.open() + } + }) } - function formatFileSize(bytes) { - if (bytes === 0) - return "0 B" - const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i] - } - - function confirmSelection() { - if (filePickerPanel.currentSelection.length === 0) - return - - root.selectedPaths = filePickerPanel.currentSelection - const path = filePickerPanel.currentSelection[0] - - if (filePickerPanel.currentSelection.length === 1) { - const isDir = folderModel.get(folderModel.indexOf(path), "fileIsDir") - if (root.selectFiles && !root.selectFolders) - root.fileSelected(path) - else if (root.selectFolders && !root.selectFiles) - root.folderSelected(path) - else - isDir ? root.folderSelected(path) : root.fileSelected(path) + function close() { + if (pickerType === "folder") { + folderDialog.close() } else { - root.filesSelected(filePickerPanel.currentSelection) + fileDialog.close() } - root.close() + + handleClose() } - function updateFilteredModel() { - filteredModel.clear() - const searchText = filePickerPanel.filterText.toLowerCase() + function handleClose() { + isOpen = false - for (var i = 0; i < folderModel.count; i++) { - const fileName = folderModel.get(i, "fileName") - const filePath = folderModel.get(i, "filePath") - const fileIsDir = folderModel.get(i, "fileIsDir") - const fileSize = folderModel.get(i, "fileSize") + if (PanelService.openedPanel !== null) { + PanelService.openedPanel.isMasked = false + } - if (root.selectFolders && !root.selectFiles && !fileIsDir) - continue - if (searchText === "" || fileName.toLowerCase().includes(searchText)) { - filteredModel.append({ - "fileName": fileName, - "filePath": filePath, - "fileIsDir": fileIsDir, - "fileSize": fileSize - }) + for (var i = 0; i < PanelService.openedPopups.length; i++) { + PanelService.openedPopups[i].isMasked = false + } + + afterClose() + } + + function reset() { + selectedPaths = [] + selectedPath = "" + } + + // Helper function to set file extensions easily + function setFileExtensions(extensions) { + if (!extensions || extensions.length === 0) { + nameFilters = ["All files (*)"] + return + } + + var filters = [] + for (var i = 0; i < extensions.length; i++) { + var ext = extensions[i] + if (typeof ext === "string") { + // Simple extension like "png" + filters.push(ext.toUpperCase() + " files (*." + ext + ")") + } else if (typeof ext === "object" && ext.label && ext.extensions) { + // Complex filter like {label: "Images", extensions: ["png", "jpg", "jpeg"]} + var filterStr = ext.label + " (" + for (var j = 0; j < ext.extensions.length; j++) { + filterStr += "*." + ext.extensions[j] + if (j < ext.extensions.length - 1) + filterStr += " " + } + filterStr += ")" + filters.push(filterStr) } } + + if (filters.length > 0) { + filters.push("All files (*)") + nameFilters = filters + } } - width: 900 * scaling - height: 700 * scaling - modal: true - closePolicy: Popup.CloseOnEscape - anchors.centerIn: Overlay.overlay - - background: Rectangle { - color: Color.mSurfaceVariant - radius: Style.radiusL * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) + // Helper function to convert URL to local path + function urlToPath(url) { + var path = url.toString() + // Remove file:// prefix (works for both Windows and Unix) + path = path.replace(/^file:\/\/\//, "/") // Unix + path = path.replace(/^file:\/\//, "") // Windows + // Handle Windows drive letters + if (Qt.platform.os === "windows") { + path = path.replace(/^\/([A-Z]:)/, "$1") + } + return path } - Rectangle { - id: filePickerPanel - anchors.fill: parent - anchors.margins: Style.marginL * scaling - color: Color.transparent - - property string filterText: "" - property var currentSelection: [] - property bool viewMode: true // true = grid, false = list - property string searchText: "" - property bool showSearchBar: false - - focus: true - - Keys.onPressed: event => { - if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) { - filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar - if (filePickerPanel.showSearchBar) - Qt.callLater(() => searchInput.forceActiveFocus()) - event.accepted = true - } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) { - filePickerPanel.showSearchBar = false - filePickerPanel.searchText = "" - filePickerPanel.filterText = "" - root.updateFilteredModel() - event.accepted = true - } - } - - ColumnLayout { - anchors.fill: parent - spacing: Style.marginM * scaling - - // Header - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NIcon { - icon: "filepicker-folder" - color: Color.mPrimary - font.pointSize: Style.fontSizeXXL * scaling - } - NText { - text: root.title - font.pointSize: Style.fontSizeXL * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.fillWidth: true - } - NIconButton { - icon: "filepicker-refresh" - tooltipText: "Refresh" - onClicked: folderModel.refresh() - } - NIconButton { - icon: "filepicker-close" - tooltipText: "Close" - onClicked: { - root.cancelled() - root.close() - } - } - } - - NDivider { - Layout.fillWidth: true - } - - // Navigation toolbar - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 45 * scaling - color: Color.mSurfaceVariant - radius: Style.radiusS * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling - - NIconButton { - icon: "filepicker-arrow-up" - tooltipText: "Up" - baseSize: Style.baseWidgetSize * 0.8 - enabled: folderModel.folder.toString() !== "file:///" - onClicked: { - const parentPath = folderModel.parentFolder.toString().replace("file://", "") - folderModel.folder = "file://" + parentPath - root.currentPath = parentPath - } - } - - NIconButton { - icon: "filepicker-home" - tooltipText: "Home" - baseSize: Style.baseWidgetSize * 0.8 - onClicked: { - const homePath = Quickshell.env("HOME") || "/home" - folderModel.folder = "file://" + homePath - root.currentPath = homePath - } - } - - NIconButton { - icon: filePickerPanel.viewMode ? "filepicker-list" : "filepicker-layout-grid" - tooltipText: filePickerPanel.viewMode ? "List View" : "Grid View" - baseSize: Style.baseWidgetSize * 0.8 - onClicked: filePickerPanel.viewMode = !filePickerPanel.viewMode - } - - NIconButton { - icon: filePickerPanel.showSearchBar ? "filepicker-x" : "filepicker-search" - tooltipText: filePickerPanel.showSearchBar ? "Close Search" : "Search" - baseSize: Style.baseWidgetSize * 0.8 - onClicked: { - filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar - if (!filePickerPanel.showSearchBar) { - filePickerPanel.searchText = "" - filePickerPanel.filterText = "" - root.updateFilteredModel() - } - } - } - - NTextInput { - id: locationInput - text: root.currentPath - placeholderText: "Enter path..." - Layout.fillWidth: true - onEditingFinished: { - const newPath = text.trim() - if (newPath !== "" && newPath !== root.currentPath) { - folderModel.folder = "file://" + newPath - root.currentPath = newPath - } else { - text = root.currentPath - } - } - Connections { - target: root - function onCurrentPathChanged() { - if (!locationInput.activeFocus) - locationInput.text = root.currentPath - } - } - } - } - } - - // Search bar - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 45 * scaling - color: Color.mSurfaceVariant - radius: Style.radiusS * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - visible: filePickerPanel.showSearchBar - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling - - NIcon { - icon: "filepicker-search" - color: Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeS * scaling - } - NTextInput { - id: searchInput - placeholderText: "Search files and folders..." - Layout.fillWidth: true - text: filePickerPanel.searchText - onTextChanged: { - filePickerPanel.searchText = text - filePickerPanel.filterText = text - root.updateFilteredModel() - } - Keys.onEscapePressed: { - filePickerPanel.showSearchBar = false - filePickerPanel.searchText = "" - filePickerPanel.filterText = "" - root.updateFilteredModel() - } - } - NIconButton { - icon: "filepicker-x" - tooltipText: "Clear" - baseSize: Style.baseWidgetSize * 0.6 - visible: filePickerPanel.searchText.length > 0 - onClicked: { - searchInput.text = "" - filePickerPanel.searchText = "" - filePickerPanel.filterText = "" - root.updateFilteredModel() - } - } - } - } - - // File list area - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: Color.mSurface - radius: Style.radiusM * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - clip: true - - FolderListModel { - id: folderModel - folder: "file://" + root.currentPath - nameFilters: root.nameFilters - showDirs: root.showDirs - showHidden: true - sortField: FolderListModel.Name - sortReversed: false - onFolderChanged: { - root.currentPath = folder.toString().replace("file://", "") - filePickerPanel.currentSelection = [] - } - onStatusChanged: { - if (status === FolderListModel.Error) { - if (root.currentPath !== Quickshell.env("HOME")) { - folder = "file://" + Quickshell.env("HOME") - root.currentPath = Quickshell.env("HOME") - } - } else if (status === FolderListModel.Ready) { - root.updateFilteredModel() - } - } - } - - ListModel { - id: filteredModel - } - - // Common scroll bar component - Component { - id: scrollBarComponent - ScrollBar { - policy: ScrollBar.AsNeeded - contentItem: Rectangle { - implicitWidth: 6 * scaling - implicitHeight: 100 - radius: Style.radiusM * scaling - color: parent.pressed ? Qt.alpha(Color.mTertiary, 0.8) : parent.hovered ? Qt.alpha(Color.mTertiary, 0.8) : Qt.alpha(Color.mTertiary, 0.8) - opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0 - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - } - } - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - } - background: Rectangle { - implicitWidth: 6 * scaling - implicitHeight: 100 - color: Color.transparent - opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0 - radius: (Style.radiusM * scaling) / 2 - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - } - } - } - } - } - - // Grid view - GridView { - id: gridView - anchors.fill: parent - anchors.margins: Style.marginM * scaling - model: filteredModel - visible: filePickerPanel.viewMode - clip: true - - property int columns: Math.max(1, Math.floor(width / (120 * scaling))) - property int itemSize: Math.floor((width - leftMargin - rightMargin - (columns * Style.marginS * scaling)) / columns) - - cellWidth: Math.floor((width - leftMargin - rightMargin) / columns) - cellHeight: Math.floor(itemSize * 0.8) + Style.marginXS * scaling + Style.fontSizeS * scaling + Style.marginM * scaling - - leftMargin: Style.marginS * scaling - rightMargin: Style.marginS * scaling - topMargin: Style.marginS * scaling - bottomMargin: Style.marginS * scaling - - ScrollBar.vertical: scrollBarComponent.createObject(gridView, { - "parent": gridView, - "x": gridView.mirrored ? 0 : gridView.width - width, - "y": 0, - "height": gridView.height - }) - - delegate: Rectangle { - id: gridItem - width: gridView.itemSize - height: gridView.cellHeight - color: Color.transparent - radius: Style.radiusM * scaling - - property bool isSelected: filePickerPanel.currentSelection.includes(model.filePath) - - Rectangle { - anchors.fill: parent - color: Color.transparent - radius: parent.radius - border.color: isSelected ? Color.mSecondary : Color.mSurface - border.width: Math.max(1, Style.borderL * scaling) - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - } - - Rectangle { - anchors.fill: parent - color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent - radius: parent.radius - border.color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent - border.width: Math.max(1, Style.borderS * scaling) - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast - } - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginXS * scaling - - Rectangle { - id: iconContainer - Layout.fillWidth: true - Layout.preferredHeight: Math.round(gridView.itemSize * 0.67) - color: Color.transparent - - property bool isImage: { - if (model.fileIsDir) - return false - const ext = model.fileName.split('.').pop().toLowerCase() - return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext) - } - - Image { - id: thumbnail - anchors.fill: parent - anchors.margins: Style.marginXS * scaling - source: iconContainer.isImage ? "file://" + model.filePath : "" - fillMode: Image.PreserveAspectFit - visible: iconContainer.isImage && status === Image.Ready - smooth: false - cache: true - asynchronous: true - sourceSize.width: 120 * scaling - sourceSize.height: 120 * scaling - onStatusChanged: { - if (status === Image.Error) - visible = false - } - - Rectangle { - anchors.fill: parent - color: Color.mSurfaceVariant - radius: Style.radiusS * scaling - visible: thumbnail.status === Image.Loading - NIcon { - icon: "filepicker-photo" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - anchors.centerIn: parent - } - } - } - - NIcon { - icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName) - font.pointSize: Style.fontSizeXXL * 2 * scaling - color: { - if (isSelected) - return Color.mSecondary - else if (mouseArea.containsMouse) - return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary - else - return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant - } - anchors.centerIn: parent - visible: !iconContainer.isImage || thumbnail.status !== Image.Ready - } - - Rectangle { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Style.marginS * scaling - width: 24 * scaling - height: 24 * scaling - radius: width / 2 - color: Color.mSecondary - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - visible: isSelected - NIcon { - icon: "filepicker-check" - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSecondary - anchors.centerIn: parent - } - } - } - - NText { - text: model.fileName - color: { - if (isSelected) - return Color.mSecondary - else if (mouseArea.containsMouse) - return Color.mOnTertiary - else - return Color.mOnSurface - } - font.pointSize: Style.fontSizeS * scaling - font.weight: isSelected ? Style.fontWeightBold : Style.fontWeightRegular - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - elide: Text.ElideRight - maximumLineCount: 2 - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - if (root.selectFolders && !root.selectFiles) { - filePickerPanel.currentSelection = [model.filePath] - } else { - folderModel.folder = "file://" + model.filePath - root.currentPath = model.filePath - } - } else { - if (root.selectFiles) - filePickerPanel.currentSelection = [model.filePath] - } - } - } - - onDoubleClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - if (root.selectFolders && !root.selectFiles) { - filePickerPanel.currentSelection = [model.filePath] - root.confirmSelection() - } else { - folderModel.folder = "file://" + model.filePath - root.currentPath = model.filePath - } - } else { - if (root.selectFiles) { - filePickerPanel.currentSelection = [model.filePath] - root.confirmSelection() - } - } - } - } - } - } - } - - // List view - ListView { - id: listView - anchors.fill: parent - anchors.margins: Style.marginS * scaling - model: filteredModel - visible: !filePickerPanel.viewMode - clip: true - - ScrollBar.vertical: scrollBarComponent.createObject(listView, { - "parent": listView, - "x": listView.mirrored ? 0 : listView.width - width, - "y": 0, - "height": listView.height - }) - - delegate: Rectangle { - id: listItem - width: listView.width - height: 40 * scaling - color: { - if (filePickerPanel.currentSelection.includes(model.filePath)) - return Color.mSecondary - if (mouseArea.containsMouse) - return Qt.alpha(Color.mOnSurface, 0.1) - return Color.transparent - } - radius: Style.radiusS * scaling - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - spacing: Style.marginM * scaling - - NIcon { - icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName) - font.pointSize: Style.fontSizeL * scaling - color: model.fileIsDir ? (filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant - } - - NText { - text: model.fileName - color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurface - font.pointSize: Style.fontSizeM * scaling - font.weight: filePickerPanel.currentSelection.includes(model.filePath) ? Style.fontWeightBold : Style.fontWeightRegular - Layout.fillWidth: true - elide: Text.ElideRight - } - - NText { - text: model.fileIsDir ? "" : root.formatFileSize(model.fileSize) - color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeS * scaling - visible: !model.fileIsDir - Layout.preferredWidth: implicitWidth - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - if (root.selectFolders && !root.selectFiles) { - filePickerPanel.currentSelection = [model.filePath] - } else { - folderModel.folder = "file://" + model.filePath - root.currentPath = model.filePath - } - } else { - if (root.selectFiles) - filePickerPanel.currentSelection = [model.filePath] - } - } - } - - onDoubleClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - if (root.selectFolders && !root.selectFiles) { - filePickerPanel.currentSelection = [model.filePath] - root.confirmSelection() - } else { - folderModel.folder = "file://" + model.filePath - root.currentPath = model.filePath - } - } else { - if (root.selectFiles) { - filePickerPanel.currentSelection = [model.filePath] - root.confirmSelection() - } - } - } - } - } - } - } - } - - // Footer - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: { - if (filePickerPanel.searchText.length > 0) { - return "Searching for: \"" + filePickerPanel.searchText + "\" (" + filteredModel.count + " matches)" - } else if (filePickerPanel.currentSelection.length > 0) { - return filePickerPanel.currentSelection.length + " item(s) selected" - } else { - return filteredModel.count + " items" - } - } - color: filePickerPanel.searchText.length > 0 ? Color.mPrimary : Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeS * scaling - Layout.fillWidth: true - } - - NButton { - text: "Cancel" - outlined: true - onClicked: { - root.cancelled() - root.close() - } - } - - NButton { - text: { - if (root.selectFolders && !root.selectFiles) - return "Select Folder" - else if (root.selectFiles && !root.selectFolders) - return "Select File" - else - return "Select" - } - icon: "filepicker-check" - enabled: filePickerPanel.currentSelection.length > 0 - onClicked: root.confirmSelection() - } - } + // Get default folder with proper fallback + function getDefaultFolder() { + if (root.initialPath) { + return "file:///" + root.initialPath.replace(/^\//, "") } - Connections { - target: root - function onShouldResetSelectionChanged() { - if (root.shouldResetSelection) { - filePickerPanel.currentSelection = [] - root.shouldResetSelection = false + // Fallback to home directory + try { + return StandardPaths.writableLocation(StandardPaths.HomeLocation) + } catch (e) { + // Final fallback if StandardPaths fails + return "file:///" + (Qt.platform.os === "windows" ? "C:/Users" : "/home") + } + } + + // FileDialog for file selection (Qt 6.x) + FileDialog { + id: fileDialog + title: root.title + currentFolder: getDefaultFolder() + fileMode: root.multipleSelection ? FileDialog.OpenFiles : FileDialog.OpenFile + nameFilters: root.nameFilters + acceptLabel: root.acceptLabel + rejectLabel: root.rejectLabel + modality: Qt.WindowModal + + onAccepted: { + if (fileMode === FileDialog.OpenFiles) { + var paths = [] + for (var i = 0; i < fileDialog.selectedFiles.length; i++) { + paths.push(urlToPath(fileDialog.selectedFiles[i])) } + root.selectedPaths = paths + root.selectedPath = paths.length > 0 ? paths[0] : "" + root.pathsSelected(paths) + root.accepted(paths) + } else { + var singlePath = urlToPath(fileDialog.selectedFile) + root.selectedPath = singlePath + root.selectedPaths = [singlePath] + root.pathSelected(singlePath) + root.accepted([singlePath]) } + root.handleClose() } - Component.onCompleted: { - if (!root.currentPath) - root.currentPath = root.initialPath - folderModel.folder = "file://" + root.currentPath + onRejected: { + root.rejected() + root.handleClose() + } + } + + // FolderDialog for folder selection (Qt 6.x) + FolderDialog { + id: folderDialog + title: root.title + currentFolder: getDefaultFolder() + acceptLabel: root.acceptLabel + rejectLabel: root.rejectLabel + modality: Qt.WindowModal + + onAccepted: { + var folderPath = urlToPath(folderDialog.selectedFolder) + root.selectedPath = folderPath + root.selectedPaths = [folderPath] + root.pathSelected(folderPath) + root.accepted([folderPath]) + root.handleClose() + } + + onRejected: { + root.rejected() + root.handleClose() } } } diff --git a/Widgets/NIconPicker.qml b/Widgets/NIconPicker.qml index bc6af0d8..c0ffaa49 100644 --- a/Widgets/NIconPicker.qml +++ b/Widgets/NIconPicker.qml @@ -41,6 +41,11 @@ Popup { selectedIcon = initialIcon query = initialIcon searchInput.forceActiveFocus() + PanelService.willOpenPopup(root) + } + + onClosed: { + PanelService.willClosePopup(root) } background: Rectangle { diff --git a/Widgets/NInputAction.qml b/Widgets/NInputAction.qml index 5b3706eb..abe03961 100644 --- a/Widgets/NInputAction.qml +++ b/Widgets/NInputAction.qml @@ -47,7 +47,6 @@ RowLayout { backgroundColor: Color.mSecondary textColor: Color.mOnSecondary hoverColor: Color.mTertiary - pressColor: Color.mPrimary enabled: root.actionButtonEnabled onClicked: { diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 26ad0b8b..03837a97 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -25,6 +25,8 @@ Loader { property bool panelAnchorLeft: false property bool panelAnchorRight: false + property bool isMasked: false + // Properties to support positioning relative to the opener (button) property bool useButtonPosition: false property point buttonPosition: Qt.point(0, 0) @@ -177,12 +179,18 @@ Loader { } visible: true - color: Settings.data.general.dimDesktop ? Qt.alpha(Color.mShadow, dimmingOpacity) : Color.transparent + WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "noctalia-panel" WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + mask: root.isMasked ? maskRegion : null + + Region { + id: maskRegion + } + Behavior on color { ColorAnimation { duration: Style.animationSlow @@ -244,7 +252,7 @@ Loader { } scale: root.scaleValue - opacity: root.opacityValue + opacity: root.isMasked ? 0 : root.opacityValue x: isDragged ? manualX : calculatedX y: isDragged ? manualY : calculatedY diff --git a/Widgets/NSearchableComboBox.qml b/Widgets/NSearchableComboBox.qml index 9093f4a5..27fb2a0d 100644 --- a/Widgets/NSearchableComboBox.qml +++ b/Widgets/NSearchableComboBox.qml @@ -153,6 +153,14 @@ RowLayout { height: root.popupHeight + 60 * scaling padding: Style.marginM * scaling + onOpened: { + PanelService.willOpenPopup(root) + } + + onClosed: { + PanelService.willClosePopup(root) + } + contentItem: ColumnLayout { spacing: Style.marginS * scaling @@ -166,13 +174,13 @@ RowLayout { fontSize: Style.fontSizeS * scaling } - ListView { + NListView { id: listView Layout.fillWidth: true Layout.fillHeight: true - clip: true model: combo.popup.visible ? filteredModel : null - ScrollIndicator.vertical: ScrollIndicator {} + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded delegate: ItemDelegate { width: listView.width diff --git a/Widgets/NText.qml b/Widgets/NText.qml index c15198d1..82d533a5 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets @@ -14,4 +15,6 @@ Text { color: Color.mOnSurface renderType: Text.QtRendering verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + wrapMode: Text.NoWrap } diff --git a/Widgets/NInputButton.qml b/Widgets/NTextInputButton.qml similarity index 100% rename from Widgets/NInputButton.qml rename to Widgets/NTextInputButton.qml diff --git a/shell.qml b/shell.qml index 22f4f521..b7418b24 100644 --- a/shell.qml +++ b/shell.qml @@ -6,32 +6,41 @@ * but proper credit must be given to the original author. */ -// Disable reload popup add this as a new row: //pragma Env QS_NO_RELOAD_POPUP=1 +// Qt & Quickshell Core import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import Quickshell.Widgets + +// Commons & Services import qs.Commons -import qs.Modules.Launcher -import qs.Modules.Background -import qs.Modules.Bar -import qs.Modules.Bar.Extras -import qs.Modules.BluetoothPanel -import qs.Modules.Calendar -import qs.Modules.Dock -import qs.Modules.IPC -import qs.Modules.LockScreen -import qs.Modules.Notification -import qs.Modules.SettingsPanel -import qs.Modules.PowerPanel -import qs.Modules.SidePanel -import qs.Modules.Toast -import qs.Modules.WiFiPanel -import qs.Modules.WallpaperSelector import qs.Services import qs.Widgets +// Core Modules +import qs.Modules.Background +import qs.Modules.Dock +import qs.Modules.LockScreen +import qs.Modules.SessionMenu + +// Bar & Bar Components +import qs.Modules.Bar +import qs.Modules.Bar.Extras +import qs.Modules.Bar.Bluetooth +import qs.Modules.Bar.Calendar + +import qs.Modules.Bar.WiFi + +// Panels & UI Components +import qs.Modules.ControlCenter +import qs.Modules.Launcher +import qs.Modules.Notification +import qs.Modules.OSD +import qs.Modules.Settings +import qs.Modules.Toast +import qs.Modules.Wallpaper + ShellRoot { id: shellRoot @@ -51,7 +60,24 @@ ShellRoot { ToastOverlay {} - IPCManager {} + // OSD overlays for volume and brightness + OSD { + id: volumeOSD + objectName: "volumeOSD" + osdType: OSD.Type.Volume + onOsdShowing: brightnessOSD.hideOSD() + } + + OSD { + id: brightnessOSD + objectName: "brightnessOSD" + osdType: OSD.Type.Brightness + onOsdShowing: volumeOSD.hideOSD() + } + + // IPCService is treated as a service + // but it's actually an Item that needs to exists in the shell. + IPCService {} // ------------------------------ // All the NPanels @@ -60,12 +86,12 @@ ShellRoot { objectName: "launcherPanel" } - SidePanel { - id: sidePanel - objectName: "sidePanel" + ControlCenterPanel { + id: controlCenterPanel + objectName: "controlCenterPanel" } - Calendar { + CalendarPanel { id: calendarPanel objectName: "calendarPanel" } @@ -80,9 +106,9 @@ ShellRoot { objectName: "notificationHistoryPanel" } - PowerPanel { - id: powerPanel - objectName: "powerPanel" + SessionMenu { + id: sessionMenuPanel + objectName: "sessionMenuPanel" } WiFiPanel { @@ -95,13 +121,20 @@ ShellRoot { objectName: "bluetoothPanel" } - WallpaperSelector { - id: wallpaperSelector - objectName: "wallpaperSelector" + WallpaperPanel { + id: wallpaperPanel + objectName: "wallpaperPanel" } Component.onCompleted: { // Save a ref. to our lockScreen so we can access it easily PanelService.lockScreen = lockScreen } + + Connections { + target: Quickshell + function onReloadCompleted() { + Quickshell.inhibitReloadPopup() + } + } }