1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

plugins/desktop-widgets: create a new "desktop" widget plugin type

- Draggable per-monitor background layer widgets
- Add basic dms version checks on plugins
- Clock: built-in clock desktop plugin
- dgop: built-in system monitor desktop plugin
This commit is contained in:
bbedward
2025-12-17 12:08:03 -05:00
parent d082d41ab9
commit 0034926df7
27 changed files with 4135 additions and 62 deletions

View File

@@ -0,0 +1,49 @@
import QtQuick
import qs.Common
Item {
id: root
property var pluginService: null
property string pluginId: ""
property real widgetWidth: 200
property real widgetHeight: 200
property real minWidth: 100
property real minHeight: 100
property var pluginData: ({})
Component.onCompleted: loadPluginData()
onPluginServiceChanged: loadPluginData()
onPluginIdChanged: loadPluginData()
Connections {
target: pluginService
function onPluginDataChanged(changedPluginId) {
if (changedPluginId !== pluginId)
return;
loadPluginData();
}
}
function loadPluginData() {
if (!pluginService || !pluginId) {
pluginData = {};
return;
}
pluginData = SettingsData.getPluginSettingsForPlugin(pluginId);
}
function getData(key, defaultValue) {
if (!pluginService || !pluginId)
return defaultValue;
return pluginService.loadPluginData(pluginId, key, defaultValue);
}
function setData(key, value) {
if (!pluginService || !pluginId)
return;
pluginService.savePluginData(pluginId, key, value);
}
}

View File

@@ -0,0 +1,270 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
Item {
id: root
required property string pluginId
required property var pluginComponent
required property var screen
property var pluginService: null
property string variantId: ""
property var variantData: null
readonly property string settingsKey: variantId ? variantId : pluginId
readonly property bool isVariant: variantId !== "" && variantData !== null
readonly property bool usePluginService: pluginService !== null && !isVariant
readonly property string screenKey: SettingsData.getScreenDisplayName(screen)
readonly property int screenWidth: screen?.width ?? 1920
readonly property int screenHeight: screen?.height ?? 1080
readonly property bool hasSavedPosition: {
if (isVariant)
return variantData?.positions?.[screenKey]?.x !== undefined;
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopX_" + screenKey, null) !== null;
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "x", null) !== null;
}
readonly property bool hasSavedSize: {
if (isVariant)
return variantData?.positions?.[screenKey]?.width !== undefined;
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopWidth_" + screenKey, null) !== null;
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "width", null) !== null;
}
property real savedX: {
if (isVariant)
return variantData?.positions?.[screenKey]?.x ?? (screenWidth / 2 - savedWidth / 2);
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopX_" + screenKey, screenWidth / 2 - savedWidth / 2);
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "x", screenWidth / 2 - savedWidth / 2);
}
property real savedY: {
if (isVariant)
return variantData?.positions?.[screenKey]?.y ?? (screenHeight / 2 - savedHeight / 2);
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopY_" + screenKey, screenHeight / 2 - savedHeight / 2);
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "y", screenHeight / 2 - savedHeight / 2);
}
property real savedWidth: {
if (isVariant)
return variantData?.positions?.[screenKey]?.width ?? 280;
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopWidth_" + screenKey, 200);
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "width", 280);
}
property real savedHeight: {
if (isVariant)
return variantData?.positions?.[screenKey]?.height ?? 180;
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopHeight_" + screenKey, 200);
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "height", 180);
}
property real widgetX: Math.max(0, Math.min(savedX, screenWidth - widgetWidth))
property real widgetY: Math.max(0, Math.min(savedY, screenHeight - widgetHeight))
property real widgetWidth: Math.max(minWidth, Math.min(savedWidth, screenWidth))
property real widgetHeight: Math.max(minHeight, Math.min(savedHeight, screenHeight))
property real minWidth: contentLoader.item?.minWidth ?? 100
property real minHeight: contentLoader.item?.minHeight ?? 100
property bool forceSquare: contentLoader.item?.forceSquare ?? false
property bool isInteracting: dragArea.drag.active || resizeArea.pressed
function updateVariantPositions(updates) {
const positions = JSON.parse(JSON.stringify(variantData?.positions || {}));
positions[screenKey] = Object.assign({}, positions[screenKey] || {}, updates);
SettingsData.updateSystemMonitorVariant(variantId, {
positions: positions
});
}
function savePosition() {
if (isVariant && variantData) {
updateVariantPositions({
x: root.widgetX,
y: root.widgetY
});
return;
}
if (usePluginService) {
pluginService.savePluginData(pluginId, "desktopX_" + screenKey, root.widgetX);
pluginService.savePluginData(pluginId, "desktopY_" + screenKey, root.widgetY);
return;
}
SettingsData.updateDesktopWidgetPosition(pluginId, screenKey, {
x: root.widgetX,
y: root.widgetY
});
}
function saveSize() {
if (isVariant && variantData) {
updateVariantPositions({
width: root.widgetWidth,
height: root.widgetHeight
});
return;
}
if (usePluginService) {
pluginService.savePluginData(pluginId, "desktopWidth_" + screenKey, root.widgetWidth);
pluginService.savePluginData(pluginId, "desktopHeight_" + screenKey, root.widgetHeight);
return;
}
SettingsData.updateDesktopWidgetPosition(pluginId, screenKey, {
width: root.widgetWidth,
height: root.widgetHeight
});
}
PanelWindow {
id: widgetWindow
screen: root.screen
visible: root.visible
color: "transparent"
WlrLayershell.namespace: "quickshell:desktop-widget:" + root.pluginId + (root.variantId ? ":" + root.variantId : "")
WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: root.widgetX
top: root.widgetY
}
implicitWidth: root.widgetWidth
implicitHeight: root.widgetHeight
Loader {
id: contentLoader
anchors.fill: parent
sourceComponent: root.pluginComponent
onLoaded: {
if (!item)
return;
if (root.usePluginService) {
item.pluginService = root.pluginService;
item.pluginId = root.pluginId;
}
if (item.variantId !== undefined)
item.variantId = root.variantId;
if (item.variantData !== undefined)
item.variantData = Qt.binding(() => root.variantData);
if (!root.hasSavedSize) {
const defW = item.defaultWidth ?? item.widgetWidth ?? 280;
const defH = item.defaultHeight ?? item.widgetHeight ?? 180;
root.widgetWidth = Math.max(root.minWidth, Math.min(defW, root.screenWidth));
root.widgetHeight = Math.max(root.minHeight, Math.min(defH, root.screenHeight));
}
if (!root.hasSavedPosition) {
root.widgetX = Math.max(0, Math.min(root.screenWidth / 2 - root.widgetWidth / 2, root.screenWidth - root.widgetWidth));
root.widgetY = Math.max(0, Math.min(root.screenHeight / 2 - root.widgetHeight / 2, root.screenHeight - root.widgetHeight));
}
if (item.widgetWidth !== undefined)
item.widgetWidth = Qt.binding(() => contentLoader.width);
if (item.widgetHeight !== undefined)
item.widgetHeight = Qt.binding(() => contentLoader.height);
}
}
Rectangle {
id: interactionBorder
anchors.fill: parent
color: "transparent"
border.color: Theme.primary
border.width: 2
radius: Theme.cornerRadius
visible: root.isInteracting
opacity: 0.8
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
width: 48
height: 48
topLeftRadius: Theme.cornerRadius
bottomRightRadius: Theme.cornerRadius
color: Theme.primary
opacity: resizeArea.pressed ? 1 : 0.6
}
}
MouseArea {
id: dragArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.ArrowCursor
drag.target: dragProxy
drag.minimumX: 0
drag.minimumY: 0
drag.maximumX: root.screenWidth - root.widgetWidth
drag.maximumY: root.screenHeight - root.widgetHeight
onReleased: root.savePosition()
}
Item {
id: dragProxy
x: root.widgetX
y: root.widgetY
onXChanged: if (dragArea.drag.active)
root.widgetX = x
onYChanged: if (dragArea.drag.active)
root.widgetY = y
}
MouseArea {
id: resizeArea
width: 48
height: 48
anchors.right: parent.right
anchors.bottom: parent.bottom
acceptedButtons: Qt.RightButton
cursorShape: pressed ? Qt.SizeFDiagCursor : Qt.ArrowCursor
property point startPos
property real startWidth
property real startHeight
onPressed: mouse => {
startPos = mapToGlobal(mouse.x, mouse.y);
startWidth = root.widgetWidth;
startHeight = root.widgetHeight;
}
onPositionChanged: mouse => {
if (!pressed)
return;
const currentPos = mapToGlobal(mouse.x, mouse.y);
const deltaX = currentPos.x - startPos.x;
const deltaY = currentPos.y - startPos.y;
let newW = Math.max(root.minWidth, Math.min(startWidth + deltaX, root.screenWidth - root.widgetX));
let newH = Math.max(root.minHeight, Math.min(startHeight + deltaY, root.screenHeight - root.widgetY));
if (root.forceSquare) {
const size = Math.max(newW, newH);
newW = Math.min(size, root.screenWidth - root.widgetX);
newH = Math.min(size, root.screenHeight - root.widgetY);
}
root.widgetWidth = newW;
root.widgetHeight = newH;
}
onReleased: root.saveSize()
}
}
}

View File

@@ -1,6 +1,6 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
@@ -10,7 +10,7 @@ Item {
property var pluginService: null
default property list<QtObject> content
signal settingChanged()
signal settingChanged
property var variants: []
property alias variantsModel: variantsListModel
@@ -18,26 +18,35 @@ Item {
implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
height: implicitHeight
readonly property bool isDesktopPlugin: {
if (!pluginService || !pluginId)
return false;
const plugin = pluginService.availablePlugins[pluginId];
return plugin?.type === "desktop";
}
readonly property bool hasPermission: {
if (!pluginService || !pluginId) return true
const allPlugins = pluginService.availablePlugins
const plugin = allPlugins[pluginId]
if (!plugin) return true
const permissions = Array.isArray(plugin.permissions) ? plugin.permissions : []
return permissions.indexOf("settings_write") !== -1
if (!pluginService || !pluginId)
return true;
const allPlugins = pluginService.availablePlugins;
const plugin = allPlugins[pluginId];
if (!plugin)
return true;
const permissions = Array.isArray(plugin.permissions) ? plugin.permissions : [];
return permissions.indexOf("settings_write") !== -1;
}
Component.onCompleted: {
loadVariants()
loadVariants();
}
onPluginServiceChanged: {
if (pluginService) {
loadVariants()
loadVariants();
for (let i = 0; i < content.length; i++) {
const child = content[i]
const child = content[i];
if (child.loadValue) {
child.loadValue()
child.loadValue();
}
}
}
@@ -45,9 +54,9 @@ Item {
onContentChanged: {
for (let i = 0; i < content.length; i++) {
const item = content[i]
const item = content[i];
if (item instanceof Item) {
item.parent = settingsColumn
item.parent = settingsColumn;
}
}
}
@@ -56,29 +65,29 @@ Item {
target: pluginService
function onPluginDataChanged(changedPluginId) {
if (changedPluginId === pluginId) {
loadVariants()
loadVariants();
}
}
}
function loadVariants() {
if (!pluginService || !pluginId) {
variants = []
return
variants = [];
return;
}
variants = pluginService.getPluginVariants(pluginId)
syncVariantsToModel()
variants = pluginService.getPluginVariants(pluginId);
syncVariantsToModel();
}
function syncVariantsToModel() {
variantsListModel.clear()
variantsListModel.clear();
for (let i = 0; i < variants.length; i++) {
variantsListModel.append(variants[i])
variantsListModel.append(variants[i]);
}
}
onVariantsChanged: {
syncVariantsToModel()
syncVariantsToModel();
}
ListModel {
@@ -87,79 +96,76 @@ Item {
function createVariant(variantName, variantConfig) {
if (!pluginService || !pluginId) {
return null
return null;
}
return pluginService.createPluginVariant(pluginId, variantName, variantConfig)
return pluginService.createPluginVariant(pluginId, variantName, variantConfig);
}
function removeVariant(variantId) {
if (!pluginService || !pluginId) {
return
return;
}
pluginService.removePluginVariant(pluginId, variantId)
pluginService.removePluginVariant(pluginId, variantId);
}
function updateVariant(variantId, variantConfig) {
if (!pluginService || !pluginId) {
return
return;
}
pluginService.updatePluginVariant(pluginId, variantId, variantConfig)
pluginService.updatePluginVariant(pluginId, variantId, variantConfig);
}
function saveValue(key, value) {
if (!pluginService) {
return
return;
}
if (!hasPermission) {
console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission")
return
console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission");
return;
}
if (pluginService.savePluginData) {
pluginService.savePluginData(pluginId, key, value)
settingChanged()
pluginService.savePluginData(pluginId, key, value);
settingChanged();
}
}
function loadValue(key, defaultValue) {
if (pluginService && pluginService.loadPluginData) {
return pluginService.loadPluginData(pluginId, key, defaultValue)
return pluginService.loadPluginData(pluginId, key, defaultValue);
}
return defaultValue
return defaultValue;
}
function findFlickable(item) {
var current = item?.parent
var current = item?.parent;
while (current) {
if (current.contentY !== undefined && current.contentHeight !== undefined) {
return current
return current;
}
current = current.parent
current = current.parent;
}
return null
return null;
}
function ensureItemVisible(item) {
if (!item) return
if (!item)
return;
var flickable = findFlickable(root);
if (!flickable)
return;
var itemGlobalY = item.mapToItem(null, 0, 0).y;
var itemHeight = item.height;
var flickableGlobalY = flickable.mapToItem(null, 0, 0).y;
var viewportHeight = flickable.height;
var flickable = findFlickable(root)
if (!flickable) return
var itemGlobalY = item.mapToItem(null, 0, 0).y
var itemHeight = item.height
var flickableGlobalY = flickable.mapToItem(null, 0, 0).y
var viewportHeight = flickable.height
var itemRelativeY = itemGlobalY - flickableGlobalY
var viewportTop = 0
var viewportBottom = viewportHeight
var itemRelativeY = itemGlobalY - flickableGlobalY;
var viewportTop = 0;
var viewportBottom = viewportHeight;
if (itemRelativeY < viewportTop) {
flickable.contentY = Math.max(0, flickable.contentY - (viewportTop - itemRelativeY) - Theme.spacingL)
flickable.contentY = Math.max(0, flickable.contentY - (viewportTop - itemRelativeY) - Theme.spacingL);
} else if (itemRelativeY + itemHeight > viewportBottom) {
flickable.contentY = Math.min(
flickable.contentHeight - viewportHeight,
flickable.contentY + (itemRelativeY + itemHeight - viewportBottom) + Theme.spacingL
)
flickable.contentY = Math.min(flickable.contentHeight - viewportHeight, flickable.contentY + (itemRelativeY + itemHeight - viewportBottom) + Theme.spacingL);
}
}
@@ -180,5 +186,97 @@ Item {
visible: root.hasPermission
width: parent.width
spacing: Theme.spacingM
Item {
id: desktopDisplaySettings
visible: root.isDesktopPlugin
width: parent.width
height: visible ? displaySettingsColumn.implicitHeight : 0
Column {
id: displaySettingsColumn
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.3
visible: root.content.length > 0
}
StyledText {
text: I18n.tr("Display Settings")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Choose which displays show this widget")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
DankToggle {
width: parent.width
text: I18n.tr("All displays")
checked: {
const prefs = root.loadValue("displayPreferences", ["all"]);
return Array.isArray(prefs) && (prefs.includes("all") || prefs.length === 0);
}
onToggled: isChecked => {
if (isChecked) {
root.saveValue("displayPreferences", ["all"]);
} else {
root.saveValue("displayPreferences", []);
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: {
const prefs = root.loadValue("displayPreferences", ["all"]);
return !Array.isArray(prefs) || (!prefs.includes("all") && prefs.length >= 0);
}
Repeater {
model: Quickshell.screens
DankToggle {
required property var modelData
width: parent.width
text: SettingsData.getScreenDisplayName(modelData)
description: modelData.width + "×" + modelData.height
checked: {
const prefs = root.loadValue("displayPreferences", ["all"]);
if (!Array.isArray(prefs) || prefs.includes("all"))
return false;
return prefs.some(p => p.name === modelData.name);
}
onToggled: isChecked => {
var prefs = root.loadValue("displayPreferences", ["all"]);
if (!Array.isArray(prefs) || prefs.includes("all")) {
prefs = [];
}
prefs = prefs.filter(p => p.name !== modelData.name);
if (isChecked) {
prefs.push({
name: modelData.name,
model: modelData.model || ""
});
}
root.saveValue("displayPreferences", prefs);
}
}
}
}
}
}
}
}