mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
desktop-widget: initial commit
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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<var> widgets: []
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -69,6 +69,7 @@ SmartPanel {
|
||||
ColorScheme,
|
||||
LockScreen,
|
||||
ControlCenter,
|
||||
DesktopWidgets,
|
||||
OSD,
|
||||
Display,
|
||||
Dock,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user