From feee1d146cf3ba067156ad660884ff5145f8c2ea Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 14 Dec 2025 14:37:29 +0100 Subject: [PATCH] desktop-widget: initial commit --- Assets/Translations/en.json | 89 +++++ Commons/Settings.qml | 7 + Modules/DesktopWidgets/DesktopClock.qml | 150 ++++++++ Modules/DesktopWidgets/DesktopMediaPlayer.qml | 331 ++++++++++++++++++ Modules/DesktopWidgets/DesktopWeather.qml | 236 +++++++++++++ Modules/DesktopWidgets/DesktopWidgets.qml | 197 +++++++++++ .../DesktopWidgetSettingsDialog.qml | 154 ++++++++ .../WidgetSettings/ClockSettings.qml | 30 ++ .../WidgetSettings/MediaPlayerSettings.qml | 58 +++ .../WidgetSettings/WeatherSettings.qml | 30 ++ Modules/Panels/Settings/SettingsContent.qml | 10 + Modules/Panels/Settings/SettingsPanel.qml | 1 + .../Settings/Tabs/DesktopWidgetsTab.qml | 168 +++++++++ Services/Media/CavaService.qml | 4 +- Services/UI/DesktopWidgetRegistry.qml | 101 ++++++ shell.qml | 2 + 16 files changed, 1567 insertions(+), 1 deletion(-) create mode 100644 Modules/DesktopWidgets/DesktopClock.qml create mode 100644 Modules/DesktopWidgets/DesktopMediaPlayer.qml create mode 100644 Modules/DesktopWidgets/DesktopWeather.qml create mode 100644 Modules/DesktopWidgets/DesktopWidgets.qml create mode 100644 Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml create mode 100644 Modules/Panels/Settings/DesktopWidgets/WidgetSettings/ClockSettings.qml create mode 100644 Modules/Panels/Settings/DesktopWidgets/WidgetSettings/MediaPlayerSettings.qml create mode 100644 Modules/Panels/Settings/DesktopWidgets/WidgetSettings/WeatherSettings.qml create mode 100644 Modules/Panels/Settings/Tabs/DesktopWidgetsTab.qml create mode 100644 Services/UI/DesktopWidgetRegistry.qml diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 04b34ab06..095f96258 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1436,6 +1436,95 @@ }, "title": "Dock" }, + "desktop-widgets": { + "widgets": { + "section": { + "label": "Desktop Widgets", + "description": "Add and configure desktop widgets" + } + }, + "edit-mode": { + "button": { + "label": "Enter edit mode" + }, + "description": "Enable edit mode to move and reposition desktop widgets. When enabled, widgets show a drag outline and can be repositioned.", + "exit-button": "Exit edit mode", + "label": "Edit mode" + }, + "enabled": { + "description": "Enable or disable desktop widgets entirely.", + "label": "Enable desktop widgets" + }, + "general": { + "section": { + "description": "Configure widgets that appear on your desktop.", + "label": "Desktop Widgets" + } + }, + "clock": { + "enabled": { + "description": "Show a clock widget on the desktop.", + "label": "Enable clock widget" + }, + "height": { + "description": "Height of the clock widget in pixels.", + "label": "Height" + }, + "section": { + "description": "Configure the clock widget appearance.", + "label": "Clock Widget" + }, + "show-date": { + "description": "Display the current date below the time.", + "label": "Show date" + }, + "show-background": { + "description": "Show the background container for the clock widget.", + "label": "Show background" + }, + "show-seconds": { + "description": "Display seconds in the time.", + "label": "Show seconds" + }, + "width": { + "description": "Width of the clock widget in pixels.", + "label": "Width" + } + }, + "media-player": { + "enabled": { + "description": "Show a media player widget on the desktop.", + "label": "Enable media player widget" + }, + "section": { + "description": "Configure the media player widget appearance.", + "label": "Media Player Widget" + }, + "show-background": { + "description": "Show the background container for the media player widget.", + "label": "Show background" + }, + "visualizer-type": { + "description": "Choose a visualization type for the desktop media player background.", + "label": "Visualization type" + } + }, + "weather": { + "enabled": { + "description": "Show a weather widget on the desktop.", + "label": "Enable weather widget" + }, + "section": { + "description": "Configure the weather widget appearance.", + "label": "Weather Widget" + }, + "show-background": { + "description": "Show the background container for the weather widget.", + "label": "Show background" + } + }, + "title": "Desktop Widgets" + }, "general": { "fonts": { "default": { diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 9466b9971..c38a708ad 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -605,6 +605,13 @@ Singleton { property string wallpaperChange: "" property string darkModeChange: "" } + + // desktop widgets + property JsonObject desktopWidgets: JsonObject { + property bool enabled: false + property bool editMode: false + property list widgets: [] + } } // ----------------------------------------------------- diff --git a/Modules/DesktopWidgets/DesktopClock.qml b/Modules/DesktopWidgets/DesktopClock.qml new file mode 100644 index 000000000..50f39410e --- /dev/null +++ b/Modules/DesktopWidgets/DesktopClock.qml @@ -0,0 +1,150 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import qs.Commons +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + property var widgetData: null + property int widgetIndex: -1 + + readonly property var now: Time.now + + property color textColor: { + var txtColor = widgetData && widgetData.textColor ? widgetData.textColor : ""; + return (txtColor && txtColor !== "") ? txtColor : Color.mOnSurface; + } + property real fontSize: { + var size = widgetData && widgetData.fontSize ? widgetData.fontSize : 0; + return (size && size > 0) ? size : Style.fontSizeXXXL * 2.5; + } + property real widgetOpacity: (widgetData && widgetData.opacity) ? widgetData.opacity : 1.0 + property bool showSeconds: (widgetData && widgetData.showSeconds !== undefined) ? widgetData.showSeconds : true + property bool showDate: (widgetData && widgetData.showDate !== undefined) ? widgetData.showDate : true + + property bool isDragging: false + property real dragOffsetX: 0 + property real dragOffsetY: 0 + + implicitWidth: contentLayout.implicitWidth + Style.marginXL * 2 + implicitHeight: contentLayout.implicitHeight + Style.marginXL * 2 + width: implicitWidth + height: implicitHeight + + x: isDragging ? dragOffsetX : ((widgetData && widgetData.x !== undefined) ? widgetData.x : 100) + y: isDragging ? dragOffsetY : ((widgetData && widgetData.y !== undefined) ? widgetData.y : 100) + MouseArea { + id: dragArea + anchors.fill: parent + z: 1000 + enabled: Settings.data.desktopWidgets.editMode + cursorShape: enabled && isDragging ? Qt.ClosedHandCursor : (enabled ? Qt.OpenHandCursor : Qt.ArrowCursor) + hoverEnabled: true + acceptedButtons: Qt.LeftButton + + property point pressPos: Qt.point(0, 0) + + onPressed: mouse => { + pressPos = Qt.point(mouse.x, mouse.y); + dragOffsetX = root.x; + dragOffsetY = root.y; + isDragging = true; + } + + onPositionChanged: mouse => { + if (isDragging && pressed) { + var globalPos = mapToItem(root.parent, mouse.x, mouse.y); + var newX = globalPos.x - pressPos.x; + var newY = globalPos.y - pressPos.y; + + if (root.parent && root.width > 0 && root.height > 0) { + newX = Math.max(0, Math.min(newX, root.parent.width - root.width)); + newY = Math.max(0, Math.min(newY, root.parent.height - root.height)); + } + + if (root.parent && root.parent.checkCollision && root.parent.checkCollision(root, newX, newY)) { + return; + } + + dragOffsetX = newX; + dragOffsetY = newY; + } + } + + onReleased: mouse => { + if (isDragging && widgetIndex >= 0) { + var widgets = Settings.data.desktopWidgets.widgets.slice(); + if (widgetIndex < widgets.length) { + widgets[widgetIndex] = Object.assign({}, widgets[widgetIndex], { + "x": dragOffsetX, + "y": dragOffsetY + }); + Settings.data.desktopWidgets.widgets = widgets; + } + isDragging = false; + } + } + + onCanceled: { + isDragging = false; + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: -Style.marginS + color: Settings.data.desktopWidgets.editMode ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.1) : "transparent" + border.color: (Settings.data.desktopWidgets.editMode || isDragging) ? (isDragging ? Qt.rgba(textColor.r, textColor.g, textColor.b, 0.5) : Color.mPrimary) : "transparent" + border.width: Settings.data.desktopWidgets.editMode ? 3 : (isDragging ? 2 : 0) + radius: Style.radiusL + Style.marginS + z: -1 + } + + Rectangle { + id: container + anchors.fill: parent + radius: Style.radiusL + color: Color.mSurface + border { + width: 1 + color: Qt.alpha(Color.mOutline, 0.12) + } + clip: true + visible: (widgetData && widgetData.showBackground !== undefined) ? widgetData.showBackground : true + + layer.enabled: Settings.data.general.enableShadows && !root.isDragging && ((widgetData && widgetData.showBackground !== undefined) ? widgetData.showBackground : true) + layer.effect: MultiEffect { + shadowEnabled: true + shadowBlur: Style.shadowBlur * 1.5 + shadowOpacity: Style.shadowOpacity * 0.6 + shadowColor: Color.black + shadowHorizontalOffset: Settings.data.general.shadowOffsetX + shadowVerticalOffset: Settings.data.general.shadowOffsetY + blurMax: Style.shadowBlurMax + } + } + + ColumnLayout { + id: contentLayout + anchors.centerIn: parent + spacing: Style.marginL + NClock { + id: clockDisplay + Layout.alignment: Qt.AlignHCenter + now: root.now + clockStyle: Settings.data.location.analogClockInCalendar ? "analog" : "digital" + backgroundColor: Color.transparent + clockColor: textColor + progressColor: Color.mPrimary + opacity: root.widgetOpacity + height: Math.round(fontSize * 1.9) + width: height + hoursFontSize: fontSize * 0.6 + minutesFontSize: fontSize * 0.4 + } + } +} \ No newline at end of file diff --git a/Modules/DesktopWidgets/DesktopMediaPlayer.qml b/Modules/DesktopWidgets/DesktopMediaPlayer.qml new file mode 100644 index 000000000..cc1d205d0 --- /dev/null +++ b/Modules/DesktopWidgets/DesktopMediaPlayer.qml @@ -0,0 +1,331 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import qs.Commons +import qs.Services.Media +import qs.Widgets +import qs.Widgets.AudioSpectrum + +Item { + id: root + + property ShellScreen screen + property var widgetData: null + property int widgetIndex: -1 + + property bool isDragging: false + property real dragOffsetX: 0 + property real dragOffsetY: 0 + + readonly property bool showPrev: hasPlayer && MediaService.canGoPrevious + readonly property bool showNext: hasPlayer && MediaService.canGoNext + readonly property int visibleButtonCount: 1 + (showPrev ? 1 : 0) + (showNext ? 1 : 0) + readonly property int baseWidth: 400 * Style.uiScaleRatio + readonly property int buttonWidth: 32 * Style.uiScaleRatio + readonly property int buttonSpacing: Style.marginXS + readonly property int controlsWidth: visibleButtonCount * buttonWidth + (visibleButtonCount > 1 ? (visibleButtonCount - 1) * buttonSpacing : 0) + + implicitWidth: baseWidth - (3 - visibleButtonCount) * (buttonWidth + buttonSpacing) + implicitHeight: contentLayout.implicitHeight + Style.marginM * 2 + width: implicitWidth + height: implicitHeight + + x: isDragging ? dragOffsetX : ((widgetData && widgetData.x !== undefined) ? widgetData.x : 100) + y: isDragging ? dragOffsetY : ((widgetData && widgetData.y !== undefined) ? widgetData.y : 200) + + readonly property bool hasPlayer: MediaService.currentPlayer !== null + readonly property bool isPlaying: MediaService.isPlaying + + property color textColor: Color.mOnSurface + Rectangle { + anchors.fill: parent + anchors.margins: -Style.marginS + color: Settings.data.desktopWidgets.editMode ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.1) : "transparent" + border.color: (Settings.data.desktopWidgets.editMode || isDragging) ? (isDragging ? Qt.rgba(textColor.r, textColor.g, textColor.b, 0.5) : Color.mPrimary) : "transparent" + border.width: Settings.data.desktopWidgets.editMode ? 3 : (isDragging ? 2 : 0) + radius: Style.radiusL + Style.marginS + z: -1 + } + + // Material 3 styled container with elevation + Rectangle { + id: container + anchors.fill: parent + radius: Style.radiusL + color: Color.mSurface + border { + width: 1 + color: Qt.alpha(Color.mOutline, 0.12) + } + clip: true + visible: (widgetData && widgetData.showBackground !== undefined) ? widgetData.showBackground : true + + Item { + anchors.fill: parent + anchors.margins: Style.marginXS + z: 0 + clip: true + layer.enabled: true + layer.smooth: true + layer.samples: 4 + layer.effect: MultiEffect { + maskEnabled: true + maskThresholdMin: 0.95 + maskSpreadAtMin: 0.0 + maskSource: ShaderEffectSource { + sourceItem: Rectangle { + width: container.width - Style.marginXS * 2 + height: container.height - Style.marginXS * 2 + radius: Math.max(0, Style.radiusL - Style.marginXS) + color: "white" + antialiasing: true + smooth: true + } + smooth: true + mipmap: true + } + } + + Loader { + anchors.fill: parent + active: (widgetData && widgetData.visualizerType) && widgetData.visualizerType !== "" && widgetData.visualizerType !== "none" + + sourceComponent: { + var visualizerType = (widgetData && widgetData.visualizerType) ? widgetData.visualizerType : ""; + switch (visualizerType) { + case "linear": + return linearComponent; + case "mirrored": + return mirroredComponent; + case "wave": + return waveComponent; + default: + return null; + } + } + + Component { + id: linearComponent + NLinearSpectrum { + anchors.fill: parent + values: CavaService.values + fillColor: Color.mPrimary + opacity: 0.6 + } + } + + Component { + id: mirroredComponent + NMirroredSpectrum { + anchors.fill: parent + values: CavaService.values + fillColor: Color.mPrimary + opacity: 0.6 + } + } + + Component { + id: waveComponent + NWaveSpectrum { + anchors.fill: parent + values: CavaService.values + fillColor: Color.mPrimary + opacity: 0.6 + } + } + } + } + + layer.enabled: Settings.data.general.enableShadows && !root.isDragging && ((widgetData && widgetData.showBackground !== undefined) ? widgetData.showBackground : true) + layer.effect: MultiEffect { + shadowEnabled: true + shadowBlur: Style.shadowBlur * 1.5 + shadowOpacity: Style.shadowOpacity * 0.6 + shadowColor: Color.black + shadowHorizontalOffset: Settings.data.general.shadowOffsetX + shadowVerticalOffset: Settings.data.general.shadowOffsetY + blurMax: Style.shadowBlurMax + } + } + + MouseArea { + id: dragArea + anchors.fill: parent + z: 1 + enabled: Settings.data.desktopWidgets.editMode + cursorShape: enabled && isDragging ? Qt.ClosedHandCursor : (enabled ? Qt.OpenHandCursor : Qt.ArrowCursor) + hoverEnabled: true + acceptedButtons: Qt.LeftButton + propagateComposedEvents: true + + property point pressPos: Qt.point(0, 0) + property bool isDraggingWidget: false + + onPressed: mouse => { + // Don't start drag if clicking on control buttons + var clickX = mouse.x; + var clickY = mouse.y; + + var buttonArea = controlsRow.mapToItem(root, 0, 0); + var buttonWidth = controlsRow.width; + var buttonHeight = controlsRow.height; + + if (clickX >= buttonArea.x && clickX <= buttonArea.x + buttonWidth && + clickY >= buttonArea.y && clickY <= buttonArea.y + buttonHeight) { + mouse.accepted = false; + return; + } + + pressPos = Qt.point(mouse.x, mouse.y); + dragOffsetX = root.x; + dragOffsetY = root.y; + isDragging = true; + isDraggingWidget = true; + } + + onPositionChanged: mouse => { + if (isDragging && isDraggingWidget && pressed) { + var globalPos = mapToItem(root.parent, mouse.x, mouse.y); + var newX = globalPos.x - pressPos.x; + var newY = globalPos.y - pressPos.y; + + if (root.parent && root.width > 0 && root.height > 0) { + newX = Math.max(0, Math.min(newX, root.parent.width - root.width)); + newY = Math.max(0, Math.min(newY, root.parent.height - root.height)); + } + + if (root.parent && root.parent.checkCollision && root.parent.checkCollision(root, newX, newY)) { + return; + } + + dragOffsetX = newX; + dragOffsetY = newY; + } + } + + onReleased: mouse => { + if (isDragging && widgetIndex >= 0) { + var widgets = Settings.data.desktopWidgets.widgets.slice(); + if (widgetIndex < widgets.length) { + widgets[widgetIndex] = Object.assign({}, widgets[widgetIndex], { + "x": dragOffsetX, + "y": dragOffsetY + }); + Settings.data.desktopWidgets.widgets = widgets; + } + isDragging = false; + isDraggingWidget = false; + } + } + + onCanceled: { + isDragging = false; + isDraggingWidget = false; + } + } + + RowLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + z: 1 + + Item { + Layout.preferredWidth: 64 * Style.uiScaleRatio + Layout.preferredHeight: 64 * Style.uiScaleRatio + Layout.alignment: Qt.AlignVCenter + + NImageRounded { + visible: hasPlayer + anchors.fill: parent + radius: width / 2 + imagePath: MediaService.trackArtUrl + fallbackIcon: isPlaying ? "media-pause" : "media-play" + fallbackIconSize: 20 * Style.uiScaleRatio + borderWidth: 0 + } + + NIcon { + visible: !hasPlayer + anchors.centerIn: parent + icon: "disc" + pointSize: 24 + color: Color.mOnSurfaceVariant + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + NText { + Layout.fillWidth: true + text: hasPlayer ? (MediaService.trackTitle || "Unknown Track") : "No media playing" + pointSize: Style.fontSizeS + font.weight: Style.fontWeightSemiBold + color: Color.mOnSurface + elide: Text.ElideRight + maximumLineCount: 1 + } + + NText { + visible: hasPlayer && MediaService.trackArtist + Layout.fillWidth: true + text: MediaService.trackArtist || "" + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightRegular + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + RowLayout { + id: controlsRow + spacing: Style.marginXS + z: 10 + + NIconButton { + visible: showPrev + baseSize: 32 + icon: "media-prev" + enabled: hasPlayer && MediaService.canGoPrevious + colorBg: Color.mSurfaceVariant + colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant + onClicked: { + if (enabled) MediaService.previous(); + } + } + + NIconButton { + baseSize: 36 + icon: isPlaying ? "media-pause" : "media-play" + enabled: hasPlayer && (MediaService.canPlay || MediaService.canPause) + colorBg: Color.mPrimary + colorFg: Color.mOnPrimary + colorBgHover: Qt.lighter(Color.mPrimary, 1.1) + colorFgHover: Color.mOnPrimary + onClicked: { + if (enabled) { + MediaService.playPause(); + } + } + } + + NIconButton { + visible: showNext + baseSize: 32 + icon: "media-next" + enabled: hasPlayer && MediaService.canGoNext + colorBg: Color.mSurfaceVariant + colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant + onClicked: { + if (enabled) MediaService.next(); + } + } + } + } +} + diff --git a/Modules/DesktopWidgets/DesktopWeather.qml b/Modules/DesktopWidgets/DesktopWeather.qml new file mode 100644 index 000000000..ba3d56803 --- /dev/null +++ b/Modules/DesktopWidgets/DesktopWeather.qml @@ -0,0 +1,236 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import qs.Commons +import qs.Services.Location +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + property var widgetData: null + property int widgetIndex: -1 + + property bool isDragging: false + property real dragOffsetX: 0 + property real dragOffsetY: 0 + + readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null) + readonly property int currentWeatherCode: weatherReady ? LocationService.data.weather.current_weather.weathercode : 0 + readonly property real currentTemp: { + if (!weatherReady) return 0; + var temp = LocationService.data.weather.current_weather.temperature; + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp); + } + return Math.round(temp); + } + readonly property real todayMax: { + if (!weatherReady || !LocationService.data.weather.daily || LocationService.data.weather.daily.temperature_2m_max.length === 0) return 0; + var temp = LocationService.data.weather.daily.temperature_2m_max[0]; + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp); + } + return Math.round(temp); + } + readonly property real todayMin: { + if (!weatherReady || !LocationService.data.weather.daily || LocationService.data.weather.daily.temperature_2m_min.length === 0) return 0; + var temp = LocationService.data.weather.daily.temperature_2m_min[0]; + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp); + } + return Math.round(temp); + } + readonly property string tempUnit: Settings.data.location.useFahrenheit ? "F" : "C" + readonly property string locationName: { + const chunks = Settings.data.location.name.split(","); + return chunks[0]; + } + + implicitWidth: Math.max(240 * Style.uiScaleRatio, contentLayout.implicitWidth + Style.marginM * 2) + implicitHeight: 64 * Style.uiScaleRatio + Style.marginM * 2 + width: implicitWidth + height: implicitHeight + + x: isDragging ? dragOffsetX : ((widgetData && widgetData.x !== undefined) ? widgetData.x : 100) + y: isDragging ? dragOffsetY : ((widgetData && widgetData.y !== undefined) ? widgetData.y : 100) + + property color textColor: Color.mOnSurface + Rectangle { + anchors.fill: parent + anchors.margins: -Style.marginS + color: Settings.data.desktopWidgets.editMode ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.1) : "transparent" + border.color: (Settings.data.desktopWidgets.editMode || isDragging) ? (isDragging ? Qt.rgba(textColor.r, textColor.g, textColor.b, 0.5) : Color.mPrimary) : "transparent" + border.width: Settings.data.desktopWidgets.editMode ? 3 : (isDragging ? 2 : 0) + radius: Style.radiusL + Style.marginS + z: -1 + } + + Rectangle { + id: container + anchors.fill: parent + radius: Style.radiusL + color: Color.mSurface + border { + width: 1 + color: Qt.alpha(Color.mOutline, 0.12) + } + clip: true + visible: (widgetData && widgetData.showBackground !== undefined) ? widgetData.showBackground : true + + layer.enabled: Settings.data.general.enableShadows && !root.isDragging && ((widgetData && widgetData.showBackground !== undefined) ? widgetData.showBackground : true) + layer.effect: MultiEffect { + shadowEnabled: true + shadowBlur: Style.shadowBlur * 1.5 + shadowOpacity: Style.shadowOpacity * 0.6 + shadowColor: Color.black + shadowHorizontalOffset: Settings.data.general.shadowOffsetX + shadowVerticalOffset: Settings.data.general.shadowOffsetY + blurMax: Style.shadowBlurMax + } + } + + MouseArea { + id: dragArea + anchors.fill: parent + z: 1 + enabled: Settings.data.desktopWidgets.editMode + cursorShape: enabled && isDragging ? Qt.ClosedHandCursor : (enabled ? Qt.OpenHandCursor : Qt.ArrowCursor) + hoverEnabled: true + acceptedButtons: Qt.LeftButton + propagateComposedEvents: true + + property point pressPos: Qt.point(0, 0) + property bool isDraggingWidget: false + + onPressed: mouse => { + pressPos = Qt.point(mouse.x, mouse.y); + dragOffsetX = root.x; + dragOffsetY = root.y; + isDragging = true; + isDraggingWidget = true; + } + + onPositionChanged: mouse => { + if (isDragging && isDraggingWidget && pressed) { + var globalPos = mapToItem(root.parent, mouse.x, mouse.y); + var newX = globalPos.x - pressPos.x; + var newY = globalPos.y - pressPos.y; + + if (root.parent && root.width > 0 && root.height > 0) { + newX = Math.max(0, Math.min(newX, root.parent.width - root.width)); + newY = Math.max(0, Math.min(newY, root.parent.height - root.height)); + } + + if (root.parent && root.parent.checkCollision && root.parent.checkCollision(root, newX, newY)) { + return; + } + + dragOffsetX = newX; + dragOffsetY = newY; + } + } + + onReleased: mouse => { + if (isDragging && widgetIndex >= 0) { + var widgets = Settings.data.desktopWidgets.widgets.slice(); + if (widgetIndex < widgets.length) { + widgets[widgetIndex] = Object.assign({}, widgets[widgetIndex], { + "x": dragOffsetX, + "y": dragOffsetY + }); + Settings.data.desktopWidgets.widgets = widgets; + } + isDragging = false; + isDraggingWidget = false; + } + } + + onCanceled: { + isDragging = false; + isDraggingWidget = false; + } + } + + RowLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + Item { + Layout.preferredWidth: 64 * Style.uiScaleRatio + Layout.preferredHeight: 64 * Style.uiScaleRatio + Layout.alignment: Qt.AlignVCenter + + NIcon { + anchors.centerIn: parent + icon: weatherReady ? LocationService.weatherSymbolFromCode(currentWeatherCode) : "cloud" + pointSize: Style.fontSizeXXXL * 2 + color: weatherReady ? Color.mPrimary : Color.mOnSurfaceVariant + } + } + + NText { + text: weatherReady ? `${currentTemp}°${tempUnit}` : "---" + pointSize: Style.fontSizeXXXL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + Layout.alignment: Qt.AlignVCenter + + NText { + Layout.fillWidth: true + text: locationName || "No location" + pointSize: Style.fontSizeS + font.weight: Style.fontWeightRegular + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + maximumLineCount: 1 + } + + RowLayout { + spacing: Style.marginXS + visible: weatherReady && todayMax > 0 && todayMin > 0 + + NText { + text: "H:" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + NText { + text: `${todayMax}°` + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + } + + NText { + text: "•" + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + opacity: 0.5 + } + + NText { + text: "L:" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + NText { + text: `${todayMin}°` + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightMedium + color: Color.mOnSurfaceVariant + } + } + } + } +} + diff --git a/Modules/DesktopWidgets/DesktopWidgets.qml b/Modules/DesktopWidgets/DesktopWidgets.qml new file mode 100644 index 000000000..d3ac03a62 --- /dev/null +++ b/Modules/DesktopWidgets/DesktopWidgets.qml @@ -0,0 +1,197 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI +import qs.Widgets + +Variants { + id: root + model: Quickshell.screens + + delegate: Loader { + required property ShellScreen modelData + active: modelData && Settings.data.desktopWidgets.enabled + + sourceComponent: PanelWindow { + id: window + color: Color.transparent + screen: modelData + + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-desktop-widgets-" + (screen?.name || "unknown") + + anchors { + top: true + bottom: true + right: true + left: true + } + + // Check if there's a focused workspace on this screen + // Widgets only show on the currently active workspace to save resources + function getFocusedWorkspaceForScreen() { + if (!screen || !screen.name) { + return false; + } + const screenName = screen.name.toLowerCase(); + + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i); + if (ws.isFocused && ws.output && ws.output.toLowerCase() === screenName) { + return true; + } + } + return false; + } + + property bool shouldShowWidgets: getFocusedWorkspaceForScreen() + + Connections { + target: CompositorService + function onWorkspaceChanged() { + shouldShowWidgets = getFocusedWorkspaceForScreen(); + } + } + + onScreenChanged: { + shouldShowWidgets = getFocusedWorkspaceForScreen(); + } + + Item { + id: widgetsContainer + anchors.fill: parent + + // Collision detection to prevent widgets from overlapping + function checkCollision(widget, newX, newY) { + if (!widget || !widget.parent) return false; + + var widgetWidth = widget.width || 0; + var widgetHeight = widget.height || 0; + + for (var i = 0; i < widgetsContainer.children.length; i++) { + var child = widgetsContainer.children[i]; + + // Skip self, container, and edit mode button (widgets can overlap button) + if (child === widget || child === widgetsContainer || child === editModeButton) { + continue; + } + + var otherWidget = null; + + // Handle Loader items - get the actual widget from the Loader + if (child.toString().indexOf("Loader") !== -1) { + if (!child.active || !child.item) { + continue; + } + otherWidget = child.item; + } else { + otherWidget = child; + } + + if (!otherWidget || !otherWidget.visible) { + continue; + } + + if (otherWidget === widget) { + continue; + } + + var otherX = otherWidget.x || 0; + var otherY = otherWidget.y || 0; + var otherWidth = otherWidget.width || 0; + var otherHeight = otherWidget.height || 0; + + // AABB overlap check + if (newX < otherX + otherWidth && + newX + widgetWidth > otherX && + newY < otherY + otherHeight && + newY + widgetHeight > otherY) { + return true; + } + } + + return false; + } + + // Load widgets dynamically from array + Repeater { + model: Settings.data.desktopWidgets.widgets || [] + + delegate: Loader { + id: widgetLoader + active: shouldShowWidgets && DesktopWidgetRegistry.hasWidget(modelData.id) + + property var widgetData: modelData + property int widgetIndex: index + + sourceComponent: { + var component = DesktopWidgetRegistry.getWidget(modelData.id); + if (component) { + return component; + } + return null; + } + + onLoaded: { + if (item) { + item.screen = window.screen; + item.parent = widgetsContainer; + item.widgetData = widgetData; + item.widgetIndex = widgetIndex; + // Set position from settings + if (widgetData.x !== undefined) { + item.x = widgetData.x; + } + if (widgetData.y !== undefined) { + item.y = widgetData.y; + } + } + } + } + } + + // Exit edit mode button + NButton { + id: editModeButton + visible: Settings.data.desktopWidgets.editMode && Settings.data.desktopWidgets.enabled + + readonly property string barPos: Settings.data.bar.position || "top" + readonly property bool barFloating: Settings.data.bar.floating || false + // Calculate offset from bar based on position and floating state + readonly property int barOffsetTop: { + if (barPos !== "top") return Style.marginXL * Style.uiScaleRatio; + const floatMarginV = barFloating ? Math.ceil(Settings.data.bar.marginVertical * Style.marginXL) : 0; + return Style.barHeight + floatMarginV + Style.marginM + (Style.marginXL * Style.uiScaleRatio); + } + readonly property int barOffsetRight: { + if (barPos !== "right") return Style.marginXL * Style.uiScaleRatio; + const floatMarginH = barFloating ? Math.ceil(Settings.data.bar.marginHorizontal * Style.marginXL) : 0; + return Style.barHeight + floatMarginH + Style.marginM + (Style.marginXL * Style.uiScaleRatio); + } + + anchors { + top: parent.top + right: parent.right + topMargin: barOffsetTop + rightMargin: barOffsetRight + } + text: I18n.tr("settings.desktop-widgets.edit-mode.exit-button") + icon: "check" + backgroundColor: Color.mSurface + textColor: Color.mOnSurface + hoverColor: Color.mSurfaceVariant + outlined: false + fontSize: Style.fontSizeM * 1.1 + iconSize: Style.fontSizeL * 1.1 + z: 10000 + onClicked: Settings.data.desktopWidgets.editMode = false + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml b/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml new file mode 100644 index 000000000..21b081f4c --- /dev/null +++ b/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml @@ -0,0 +1,154 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Popup { + id: root + + property int widgetIndex: -1 + property var widgetData: null + property string widgetId: "" + property string sectionId: "" // Not used for desktop widgets, but required by NSectionEditor + + signal updateWidgetSettings(int index, var settings) + + width: Math.max(content.implicitWidth + padding * 2, 500) + height: content.implicitHeight + padding * 2 + padding: Style.marginXL + modal: true + dim: false + anchors.centerIn: parent + + onOpened: { + if (widgetData && widgetId) { + loadWidgetSettings(); + } + forceActiveFocus(); + } + + background: Rectangle { + id: bgRect + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: FocusScope { + id: focusScope + focus: true + + ColumnLayout { + id: content + anchors.fill: parent + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + + NText { + text: I18n.tr("system.widget-settings-title", { + "widget": root.widgetId + }) + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("tooltips.close") + onClicked: root.close() + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + Loader { + id: settingsLoader + Layout.fillWidth: true + onLoaded: { + if (item) { + Qt.callLater(() => { + var firstInput = findFirstFocusable(item); + if (firstInput) { + firstInput.forceActiveFocus(); + } else { + focusScope.forceActiveFocus(); + } + }); + } + } + + function findFirstFocusable(item) { + if (!item) return null; + if (item.focus !== undefined && item.focus === true) return item; + if (item.children) { + for (var i = 0; i < item.children.length; i++) { + var child = item.children[i]; + if (child && child.focus !== undefined && child.focus === true) return child; + var found = findFirstFocusable(child); + if (found) return found; + } + } + return null; + } + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + spacing: Style.marginM + + Item { + Layout.fillWidth: true + } + + NButton { + text: I18n.tr("bar.widget-settings.dialog.cancel") + outlined: true + onClicked: root.close() + } + + NButton { + text: I18n.tr("bar.widget-settings.dialog.apply") + icon: "check" + onClicked: { + if (settingsLoader.item && settingsLoader.item.saveSettings) { + var newSettings = settingsLoader.item.saveSettings(); + root.updateWidgetSettings(root.widgetIndex, newSettings); + root.close(); + } + } + } + } + } + } + + function loadWidgetSettings() { + const source = DesktopWidgetRegistry.widgetSettingsMap[widgetId]; + if (source) { + var currentWidgetData = widgetData; + var widgets = Settings.data.desktopWidgets.widgets; + if (widgets && widgetIndex >= 0 && widgetIndex < widgets.length) { + currentWidgetData = widgets[widgetIndex]; + } + var fullPath = Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/" + source); + settingsLoader.setSource(fullPath, { + "widgetData": currentWidgetData, + "widgetMetadata": DesktopWidgetRegistry.widgetMetadata[widgetId] + }); + } + } +} + diff --git a/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/ClockSettings.qml b/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/ClockSettings.qml new file mode 100644 index 000000000..affdbe00f --- /dev/null +++ b/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/ClockSettings.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + property var widgetData: null + property var widgetMetadata: null + + property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : (widgetMetadata ? widgetMetadata.showBackground : true) + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.showBackground = valueShowBackground; + return settings; + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("settings.desktop-widgets.clock.show-background.label") + description: I18n.tr("settings.desktop-widgets.clock.show-background.description") + checked: valueShowBackground + onToggled: checked => valueShowBackground = checked + } +} + diff --git a/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/MediaPlayerSettings.qml b/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/MediaPlayerSettings.qml new file mode 100644 index 000000000..12b763b2d --- /dev/null +++ b/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/MediaPlayerSettings.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + property var widgetData: null + property var widgetMetadata: null + + property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : (widgetMetadata ? widgetMetadata.showBackground : true) + property string valueVisualizerType: widgetData.visualizerType !== undefined ? widgetData.visualizerType : (widgetMetadata ? widgetMetadata.visualizerType : "") + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.showBackground = valueShowBackground; + settings.visualizerType = valueVisualizerType; + return settings; + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("settings.desktop-widgets.media-player.show-background.label") + description: I18n.tr("settings.desktop-widgets.media-player.show-background.description") + checked: valueShowBackground + onToggled: checked => valueShowBackground = checked + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("settings.desktop-widgets.media-player.visualizer-type.label") + description: I18n.tr("settings.desktop-widgets.media-player.visualizer-type.description") + model: [ + { + "key": "", + "name": I18n.tr("options.visualizer-types.none") + }, + { + "key": "linear", + "name": I18n.tr("options.visualizer-types.linear") + }, + { + "key": "mirrored", + "name": I18n.tr("options.visualizer-types.mirrored") + }, + { + "key": "wave", + "name": I18n.tr("options.visualizer-types.wave") + } + ] + currentKey: valueVisualizerType + onSelected: key => valueVisualizerType = key + } +} + diff --git a/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/WeatherSettings.qml b/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/WeatherSettings.qml new file mode 100644 index 000000000..b852e02e0 --- /dev/null +++ b/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/WeatherSettings.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + property var widgetData: null + property var widgetMetadata: null + + property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : (widgetMetadata ? widgetMetadata.showBackground : true) + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.showBackground = valueShowBackground; + return settings; + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("settings.desktop-widgets.weather.show-background.label") + description: I18n.tr("settings.desktop-widgets.weather.show-background.description") + checked: valueShowBackground + onToggled: checked => valueShowBackground = checked + } +} + diff --git a/Modules/Panels/Settings/SettingsContent.qml b/Modules/Panels/Settings/SettingsContent.qml index 0f05c4db2..ea33d0cde 100644 --- a/Modules/Panels/Settings/SettingsContent.qml +++ b/Modules/Panels/Settings/SettingsContent.qml @@ -121,6 +121,10 @@ Item { id: pluginsTab PluginsTab {} } + Component { + id: desktopWidgetsTab + DesktopWidgetsTab {} + } function updateTabsModel() { let newTabs = [ @@ -160,6 +164,12 @@ Item { "icon": "settings-dock", "source": dockTab }, + { + "id": SettingsPanel.Tab.DesktopWidgets, + "label": "settings.desktop-widgets.title", + "icon": "clock", + "source": desktopWidgetsTab + }, { "id": SettingsPanel.Tab.ControlCenter, "label": "settings.control-center.title", diff --git a/Modules/Panels/Settings/SettingsPanel.qml b/Modules/Panels/Settings/SettingsPanel.qml index aae7ebecd..c47f49fa7 100644 --- a/Modules/Panels/Settings/SettingsPanel.qml +++ b/Modules/Panels/Settings/SettingsPanel.qml @@ -69,6 +69,7 @@ SmartPanel { ColorScheme, LockScreen, ControlCenter, + DesktopWidgets, OSD, Display, Dock, diff --git a/Modules/Panels/Settings/Tabs/DesktopWidgetsTab.qml b/Modules/Panels/Settings/Tabs/DesktopWidgetsTab.qml new file mode 100644 index 000000000..500412959 --- /dev/null +++ b/Modules/Panels/Settings/Tabs/DesktopWidgetsTab.qml @@ -0,0 +1,168 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + + spacing: Style.marginL + + NHeader { + label: I18n.tr("settings.desktop-widgets.general.section.label") + description: I18n.tr("settings.desktop-widgets.general.section.description") + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("settings.desktop-widgets.enabled.label") + description: I18n.tr("settings.desktop-widgets.enabled.description") + checked: Settings.data.desktopWidgets.enabled + onToggled: checked => Settings.data.desktopWidgets.enabled = checked + } + + NButton { + visible: Settings.data.desktopWidgets.enabled + Layout.fillWidth: true + text: I18n.tr("settings.desktop-widgets.edit-mode.button.label") + icon: "edit" + onClicked: { + Settings.data.desktopWidgets.editMode = true + if (Settings.data.ui.settingsPanelMode !== "window") { + var item = root.parent + while (item) { + if (item.closeRequested !== undefined) { + item.closeRequested() + break + } + item = item.parent + } + } + } + } + + NDivider { + visible: Settings.data.desktopWidgets.enabled + Layout.fillWidth: true + } + + // Desktop Widgets Section + NSectionEditor { + visible: Settings.data.desktopWidgets.enabled + Layout.fillWidth: true + sectionName: I18n.tr("settings.desktop-widgets.widgets.section.label") + sectionId: "desktop" + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml") + widgetRegistry: DesktopWidgetRegistry + widgetModel: Settings.data.desktopWidgets.widgets + availableWidgets: availableWidgets + maxWidgets: -1 + onAddWidget: (widgetId, section) => _addWidget(widgetId) + onRemoveWidget: (section, index) => _removeWidget(index) + onReorderWidget: (section, fromIndex, toIndex) => _reorderWidget(fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettings(index, settings) + onMoveWidget: (fromSection, index, toSection) => {} // Not needed for desktop widgets + } + + // Available widgets model - must be a ListModel with id, not a property + ListModel { + id: availableWidgets + } + + Component.onCompleted: { + // Use Qt.callLater to ensure DesktopWidgetRegistry is ready + Qt.callLater(updateAvailableWidgetsModel); + } + + function updateAvailableWidgetsModel() { + availableWidgets.clear(); + try { + if (typeof DesktopWidgetRegistry === "undefined" || !DesktopWidgetRegistry) { + Logger.e("DesktopWidgetsTab", "DesktopWidgetRegistry is not available"); + // Retry after a short delay + Qt.callLater(function() { + if (typeof DesktopWidgetRegistry !== "undefined" && DesktopWidgetRegistry) { + updateAvailableWidgetsModel(); + } + }); + return; + } + var widgetIds = DesktopWidgetRegistry.getAvailableWidgets(); + Logger.d("DesktopWidgetsTab", "Found widgets:", widgetIds, "count:", widgetIds ? widgetIds.length : 0); + if (!widgetIds || widgetIds.length === 0) { + Logger.w("DesktopWidgetsTab", "No widgets found in registry"); + return; + } + for (var i = 0; i < widgetIds.length; i++) { + var widgetId = widgetIds[i]; + availableWidgets.append({ + "key": widgetId, + "name": widgetId + }); + } + Logger.d("DesktopWidgetsTab", "Available widgets model count:", availableWidgets.count); + } catch (e) { + Logger.e("DesktopWidgetsTab", "Error updating available widgets:", e, e.stack); + } + } + + function _addWidget(widgetId) { + var newWidget = { + "id": widgetId + }; + if (DesktopWidgetRegistry.widgetHasUserSettings(widgetId)) { + var metadata = DesktopWidgetRegistry.widgetMetadata[widgetId]; + if (metadata) { + Object.keys(metadata).forEach(function (key) { + if (key !== "allowUserSettings") { + newWidget[key] = metadata[key]; + } + }); + } + } + // Set default positions + if (widgetId === "Clock") { + newWidget.x = 50; + newWidget.y = 50; + } else if (widgetId === "MediaPlayer") { + newWidget.x = 100; + newWidget.y = 200; + } else if (widgetId === "Weather") { + newWidget.x = 100; + newWidget.y = 300; + } + var widgets = Settings.data.desktopWidgets.widgets.slice(); + widgets.push(newWidget); + Settings.data.desktopWidgets.widgets = widgets; + } + + function _removeWidget(index) { + if (index >= 0 && index < Settings.data.desktopWidgets.widgets.length) { + var newArray = Settings.data.desktopWidgets.widgets.slice(); + newArray.splice(index, 1); + Settings.data.desktopWidgets.widgets = newArray; + } + } + + function _reorderWidget(fromIndex, toIndex) { + if (fromIndex >= 0 && fromIndex < Settings.data.desktopWidgets.widgets.length && + toIndex >= 0 && toIndex < Settings.data.desktopWidgets.widgets.length) { + var newArray = Settings.data.desktopWidgets.widgets.slice(); + var item = newArray[fromIndex]; + newArray.splice(fromIndex, 1); + newArray.splice(toIndex, 0, item); + Settings.data.desktopWidgets.widgets = newArray; + } + } + + function _updateWidgetSettings(index, settings) { + if (index >= 0 && index < Settings.data.desktopWidgets.widgets.length) { + var newArray = Settings.data.desktopWidgets.widgets.slice(); + newArray[index] = Object.assign({}, newArray[index], settings); + Settings.data.desktopWidgets.widgets = newArray; + } + } +} diff --git a/Services/Media/CavaService.qml b/Services/Media/CavaService.qml index af2d31962..04fac920c 100644 --- a/Services/Media/CavaService.qml +++ b/Services/Media/CavaService.qml @@ -14,8 +14,10 @@ Singleton { * - Bar has an audio visualizer * - LockScreen is opened * - A control center is open + * - Desktop media player has a visualizer enabled */ - property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen?.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) + readonly property bool hasDesktopMediaVisualizer: Settings.data.desktopWidgets.mediaPlayer.enabled && Settings.data.desktopWidgets.mediaPlayer.visualizerType !== "" && Settings.data.desktopWidgets.mediaPlayer.visualizerType !== "none" + property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen?.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) || hasDesktopMediaVisualizer property var values: [] property int barsCount: 32 diff --git a/Services/UI/DesktopWidgetRegistry.qml b/Services/UI/DesktopWidgetRegistry.qml new file mode 100644 index 000000000..32baa8131 --- /dev/null +++ b/Services/UI/DesktopWidgetRegistry.qml @@ -0,0 +1,101 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.DesktopWidgets + +Singleton { + id: root + + // Component definitions + property Component clockComponent: Component { + DesktopClock {} + } + property Component mediaPlayerComponent: Component { + DesktopMediaPlayer {} + } + property Component weatherComponent: Component { + DesktopWeather {} + } + + // Widget registry object mapping widget names to components + // Created in Component.onCompleted to ensure Components are ready + property var widgets: ({}) + + Component.onCompleted: { + // Initialize widgets object after Components are ready + var widgetsObj = {}; + widgetsObj["Clock"] = clockComponent; + widgetsObj["MediaPlayer"] = mediaPlayerComponent; + widgetsObj["Weather"] = weatherComponent; + widgets = widgetsObj; + + Logger.i("DesktopWidgetRegistry", "Service started"); + Logger.d("DesktopWidgetRegistry", "Available widgets:", Object.keys(widgets)); + Logger.d("DesktopWidgetRegistry", "Clock component:", clockComponent ? "exists" : "null"); + Logger.d("DesktopWidgetRegistry", "MediaPlayer component:", mediaPlayerComponent ? "exists" : "null"); + Logger.d("DesktopWidgetRegistry", "Weather component:", weatherComponent ? "exists" : "null"); + Logger.d("DesktopWidgetRegistry", "Widgets object keys:", Object.keys(widgets)); + Logger.d("DesktopWidgetRegistry", "Widgets object values check - Clock:", widgets["Clock"] ? "exists" : "null"); + } + + property var widgetSettingsMap: ({ + "Clock": "WidgetSettings/ClockSettings.qml", + "MediaPlayer": "WidgetSettings/MediaPlayerSettings.qml", + "Weather": "WidgetSettings/WeatherSettings.qml" + }) + + property var widgetMetadata: ({ + "Clock": { + "allowUserSettings": true, + "showBackground": true + }, + "MediaPlayer": { + "allowUserSettings": true, + "showBackground": true, + "visualizerType": "" + }, + "Weather": { + "allowUserSettings": true, + "showBackground": true + } + }) + + function init() { + Logger.i("DesktopWidgetRegistry", "Service started"); + } + + // Helper function to get widget component by name + function getWidget(id) { + return widgets[id] || null; + } + + // Helper function to check if widget exists + function hasWidget(id) { + return id in widgets; + } + + // Get list of available widget ids + function getAvailableWidgets() { + var keys = Object.keys(widgets); + Logger.d("DesktopWidgetRegistry", "getAvailableWidgets() called, returning:", keys); + return keys; + } + + // Helper function to check if widget has user settings + function widgetHasUserSettings(id) { + return (widgetMetadata[id] !== undefined) && (widgetMetadata[id].allowUserSettings === true); + } + + // Check if a widget is a plugin widget (desktop widgets don't support plugins yet) + function isPluginWidget(id) { + return false; + } + + // Get list of plugin widget IDs (empty for now) + function getPluginWidgets() { + return []; + } +} + diff --git a/shell.qml b/shell.qml index aa44c14db..eec1ae707 100644 --- a/shell.qml +++ b/shell.qml @@ -16,6 +16,7 @@ import qs.Commons // Modules import qs.Modules.Background import qs.Modules.Bar +import qs.Modules.DesktopWidgets import qs.Modules.Dock import qs.Modules.LockScreen import qs.Modules.MainScreen @@ -108,6 +109,7 @@ ShellRoot { Overview {} Background {} + DesktopWidgets {} AllScreens {} Dock {} Notification {}