desktop-widget: initial commit

This commit is contained in:
Ly-sec
2025-12-14 14:37:29 +01:00
parent 76bcaa2a50
commit feee1d146c
16 changed files with 1567 additions and 1 deletions
+89
View File
@@ -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": {
+7
View File
@@ -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: []
}
}
// -----------------------------------------------------
+150
View File
@@ -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();
}
}
}
}
}
+236
View File
@@ -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
}
}
}
}
}
+197
View File
@@ -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;
}
}
}
+3 -1
View File
@@ -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
+101
View File
@@ -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 [];
}
}
+2
View File
@@ -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 {}