import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import qs.Common import qs.Services import qs.Widgets Item { id: root required property string pluginId required property var screen property var builtinComponent: null property var pluginService: null property string instanceId: "" property var instanceData: null property bool widgetEnabled: true readonly property bool isBuiltin: pluginId === "desktopClock" || pluginId === "systemMonitor" readonly property var activeComponent: isBuiltin ? builtinComponent : PluginService.pluginDesktopComponents[pluginId] ?? null readonly property bool showOnOverlay: instanceData?.config?.showOnOverlay ?? false readonly property bool showOnOverview: instanceData?.config?.showOnOverview ?? false readonly property bool showOnOverviewOnly: instanceData?.config?.showOnOverviewOnly ?? false readonly property bool overviewActive: CompositorService.isNiri && NiriService.inOverview readonly property bool clickThrough: instanceData?.config?.clickThrough ?? false readonly property bool syncPositionAcrossScreens: instanceData?.config?.syncPositionAcrossScreens ?? false Connections { target: PluginService enabled: !root.isBuiltin function onPluginLoaded(loadedPluginId) { if (loadedPluginId === root.pluginId) contentLoader.reloadComponent(); } function onPluginUnloaded(unloadedPluginId) { if (unloadedPluginId === root.pluginId) contentLoader.reloadComponent(); } } readonly property string settingsKey: instanceId ? instanceId : pluginId readonly property bool isInstance: instanceId !== "" && instanceData !== null readonly property bool usePluginService: pluginService !== null && !isInstance QtObject { id: instanceScopedPluginService readonly property var availablePlugins: PluginService.availablePlugins readonly property var loadedPlugins: PluginService.loadedPlugins readonly property var pluginDesktopComponents: PluginService.pluginDesktopComponents signal pluginDataChanged(string pluginId) signal pluginLoaded(string pluginId) signal pluginUnloaded(string pluginId) function loadPluginData(pluginId, key, defaultValue) { const cfg = root.instanceData?.config; if (cfg && key in cfg) return cfg[key]; return SettingsData.getPluginSetting(pluginId, key, defaultValue); } function savePluginData(pluginId, key, value) { if (!root.instanceId) return false; var updates = {}; updates[key] = value; SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, updates); Qt.callLater(() => pluginDataChanged(pluginId)); return true; } function getPluginVariants(pluginId) { return PluginService.getPluginVariants(pluginId); } function isPluginLoaded(pluginId) { return PluginService.isPluginLoaded(pluginId); } } readonly property string screenKey: SettingsData.getScreenDisplayName(screen) readonly property string positionKey: syncPositionAcrossScreens ? "_synced" : screenKey readonly property int screenWidth: screen?.width ?? 1920 readonly property int screenHeight: screen?.height ?? 1080 readonly property bool useGhostPreview: !CompositorService.isNiri property real previewX: widgetX property real previewY: widgetY property real previewWidth: widgetWidth property real previewHeight: widgetHeight readonly property bool hasSavedPosition: { if (isInstance) return instanceData?.positions?.[positionKey]?.x !== undefined; if (usePluginService) return pluginService.loadPluginData(pluginId, "desktopX_" + positionKey, null) !== null; return SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "x", null) !== null; } readonly property bool hasSavedSize: { if (isInstance) return instanceData?.positions?.[positionKey]?.width !== undefined; if (usePluginService) return pluginService.loadPluginData(pluginId, "desktopWidth_" + positionKey, null) !== null; return SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "width", null) !== null; } property real savedX: { if (isInstance) { const val = instanceData?.positions?.[positionKey]?.x; if (val === undefined) return screenWidth / 2 - savedWidth / 2; return syncPositionAcrossScreens ? val * screenWidth : val; } if (usePluginService) { const val = pluginService.loadPluginData(pluginId, "desktopX_" + positionKey, null); if (val === null) return screenWidth / 2 - savedWidth / 2; return syncPositionAcrossScreens ? val * screenWidth : val; } const val = SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "x", null); if (val === null) return screenWidth / 2 - savedWidth / 2; return syncPositionAcrossScreens ? val * screenWidth : val; } property real savedY: { if (isInstance) { const val = instanceData?.positions?.[positionKey]?.y; if (val === undefined) return screenHeight / 2 - savedHeight / 2; return syncPositionAcrossScreens ? val * screenHeight : val; } if (usePluginService) { const val = pluginService.loadPluginData(pluginId, "desktopY_" + positionKey, null); if (val === null) return screenHeight / 2 - savedHeight / 2; return syncPositionAcrossScreens ? val * screenHeight : val; } const val = SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "y", null); if (val === null) return screenHeight / 2 - savedHeight / 2; return syncPositionAcrossScreens ? val * screenHeight : val; } property real savedWidth: { if (isInstance) { const val = instanceData?.positions?.[positionKey]?.width; if (val === undefined) return 280; return val; } if (usePluginService) { const val = pluginService.loadPluginData(pluginId, "desktopWidth_" + positionKey, null); if (val === null) return 200; return val; } const val = SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "width", null); if (val === null) return 280; return val; } property real savedHeight: { if (isInstance) { const val = instanceData?.positions?.[positionKey]?.height; if (val === undefined) return forceSquare ? savedWidth : 180; return forceSquare ? savedWidth : val; } if (usePluginService) { const val = pluginService.loadPluginData(pluginId, "desktopHeight_" + positionKey, null); if (val === null) return forceSquare ? savedWidth : 200; return forceSquare ? savedWidth : val; } const val = SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "height", null); if (val === null) return forceSquare ? savedWidth : 180; return forceSquare ? savedWidth : val; } property real dragOverrideX: -1 property real dragOverrideY: -1 property real dragOverrideW: -1 property real dragOverrideH: -1 readonly property real effectiveX: dragOverrideX >= 0 ? dragOverrideX : savedX readonly property real effectiveY: dragOverrideY >= 0 ? dragOverrideY : savedY readonly property real effectiveW: dragOverrideW >= 0 ? dragOverrideW : savedWidth readonly property real effectiveH: dragOverrideH >= 0 ? dragOverrideH : savedHeight readonly property real widgetX: Math.max(0, Math.min(effectiveX, screenWidth - widgetWidth)) readonly property real widgetY: Math.max(0, Math.min(effectiveY, screenHeight - widgetHeight)) readonly property real widgetWidth: Math.max(minWidth, Math.min(effectiveW, screenWidth)) readonly property real widgetHeight: Math.max(minHeight, Math.min(effectiveH, screenHeight)) function clearDragOverrides() { dragOverrideX = -1; dragOverrideY = -1; dragOverrideW = -1; dragOverrideH = -1; } 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.pressed || resizeArea.pressed property var _gridSettingsTrigger: SettingsData.desktopWidgetGridSettings readonly property int gridSize: { void _gridSettingsTrigger; return SettingsData.getDesktopWidgetGridSetting(screenKey, "size", 40); } readonly property bool gridEnabled: { void _gridSettingsTrigger; return SettingsData.getDesktopWidgetGridSetting(screenKey, "enabled", false); } function snapToGrid(value) { return Math.round(value / gridSize) * gridSize; } function savePosition(finalX, finalY) { const xVal = syncPositionAcrossScreens ? finalX / screenWidth : finalX; const yVal = syncPositionAcrossScreens ? finalY / screenHeight : finalY; if (isInstance && instanceData) { SettingsData.updateDesktopWidgetInstancePosition(instanceId, positionKey, { x: xVal, y: yVal }); return; } if (usePluginService) { pluginService.savePluginData(pluginId, "desktopX_" + positionKey, xVal); pluginService.savePluginData(pluginId, "desktopY_" + positionKey, yVal); return; } SettingsData.updateDesktopWidgetPosition(pluginId, positionKey, { x: xVal, y: yVal }); } function saveSize(finalW, finalH) { const sizeVal = forceSquare ? Math.max(finalW, finalH) : finalW; const heightVal = forceSquare ? sizeVal : finalH; if (isInstance && instanceData) { SettingsData.updateDesktopWidgetInstancePosition(instanceId, positionKey, { width: sizeVal, height: heightVal }); return; } if (usePluginService) { pluginService.savePluginData(pluginId, "desktopWidth_" + positionKey, sizeVal); pluginService.savePluginData(pluginId, "desktopHeight_" + positionKey, heightVal); return; } SettingsData.updateDesktopWidgetPosition(pluginId, positionKey, { width: sizeVal, height: heightVal }); } PanelWindow { id: widgetWindow screen: root.screen visible: { if (!root.widgetEnabled || root.activeComponent === null) return false; if (root.showOnOverviewOnly) return root.overviewActive; return true; } color: "transparent" Region { id: emptyMask } mask: root.clickThrough ? emptyMask : null WlrLayershell.namespace: "quickshell:desktop-widget:" + root.pluginId + (root.instanceId ? ":" + root.instanceId : "") WlrLayershell.layer: { if (root.isInteracting && !CompositorService.useHyprlandFocusGrab) return WlrLayer.Overlay; if (root.showOnOverlay) return WlrLayer.Overlay; if (root.overviewActive && (root.showOnOverview || root.showOnOverviewOnly)) return WlrLayer.Overlay; return WlrLayer.Bottom; } WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: { if (!root.isInteracting) return WlrKeyboardFocus.None; if (CompositorService.useHyprlandFocusGrab) return WlrKeyboardFocus.OnDemand; return WlrKeyboardFocus.Exclusive; } HyprlandFocusGrab { active: CompositorService.isHyprland && root.isInteracting windows: [widgetWindow] } Item { anchors.fill: parent focus: root.isInteracting Keys.onPressed: event => { if (!root.isInteracting) return; switch (event.key) { case Qt.Key_G: SettingsData.setDesktopWidgetGridSetting(root.screenKey, "enabled", !root.gridEnabled); event.accepted = true; break; case Qt.Key_Z: SettingsData.setDesktopWidgetGridSetting(root.screenKey, "size", Math.max(10, root.gridSize - 10)); event.accepted = true; break; case Qt.Key_X: SettingsData.setDesktopWidgetGridSetting(root.screenKey, "size", Math.min(200, root.gridSize + 10)); event.accepted = true; break; } } } 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.activeComponent function reloadComponent() { active = false; active = true; } function updateInstanceData() { if (!item || item.instanceData === undefined) return; item.instanceData = root.instanceData; } Connections { target: root enabled: contentLoader.item !== null function onInstanceDataChanged() { contentLoader.updateInstanceData(); } } onLoaded: { if (!item) return; if (item.pluginService !== undefined) { item.pluginService = root.isInstance ? instanceScopedPluginService : root.pluginService; } if (item.pluginId !== undefined) item.pluginId = root.pluginId; if (item.instanceId !== undefined) item.instanceId = root.instanceId; if (item.instanceData !== undefined) item.instanceData = root.instanceData; if (!root.hasSavedSize) { const defW = item.defaultWidth ?? item.widgetWidth ?? 280; const defH = item.defaultHeight ?? item.widgetHeight ?? 180; const finalW = Math.max(root.minWidth, Math.min(defW, root.screenWidth)); const finalH = Math.max(root.minHeight, Math.min(defH, root.screenHeight)); root.saveSize(finalW, finalH); } if (!root.hasSavedPosition) { const finalX = Math.max(0, Math.min(root.screenWidth / 2 - root.widgetWidth / 2, root.screenWidth - root.widgetWidth)); const finalY = Math.max(0, Math.min(root.screenHeight / 2 - root.widgetHeight / 2, root.screenHeight - root.widgetHeight)); root.savePosition(finalX, finalY); } 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 && !root.useGhostPreview 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 enabled: !root.clickThrough cursorShape: pressed ? Qt.ClosedHandCursor : Qt.ArrowCursor property point startPos property real startX property real startY onPressed: mouse => { startPos = root.useGhostPreview ? Qt.point(mouse.x, mouse.y) : mapToGlobal(mouse.x, mouse.y); startX = root.widgetX; startY = root.widgetY; root.previewX = root.widgetX; root.previewY = root.widgetY; root.dragOverrideX = root.widgetX; root.dragOverrideY = root.widgetY; } onPositionChanged: mouse => { if (!pressed) return; const currentPos = root.useGhostPreview ? Qt.point(mouse.x, mouse.y) : mapToGlobal(mouse.x, mouse.y); let newX = Math.max(0, Math.min(startX + currentPos.x - startPos.x, root.screenWidth - root.widgetWidth)); let newY = Math.max(0, Math.min(startY + currentPos.y - startPos.y, root.screenHeight - root.widgetHeight)); if (root.gridEnabled) { newX = Math.max(0, Math.min(root.snapToGrid(newX), root.screenWidth - root.widgetWidth)); newY = Math.max(0, Math.min(root.snapToGrid(newY), root.screenHeight - root.widgetHeight)); } if (root.useGhostPreview) { root.previewX = newX; root.previewY = newY; return; } root.dragOverrideX = newX; root.dragOverrideY = newY; } onReleased: { const finalX = root.useGhostPreview ? root.previewX : root.dragOverrideX; const finalY = root.useGhostPreview ? root.previewY : root.dragOverrideY; root.savePosition(finalX, finalY); root.clearDragOverrides(); } } MouseArea { id: resizeArea width: 48 height: 48 anchors.right: parent.right anchors.bottom: parent.bottom acceptedButtons: Qt.RightButton enabled: !root.clickThrough cursorShape: pressed ? Qt.SizeFDiagCursor : Qt.ArrowCursor property point startPos property real startWidth property real startHeight onPressed: mouse => { startPos = root.useGhostPreview ? Qt.point(mouse.x, mouse.y) : mapToGlobal(mouse.x, mouse.y); startWidth = root.widgetWidth; startHeight = root.widgetHeight; root.previewWidth = root.widgetWidth; root.previewHeight = root.widgetHeight; root.dragOverrideW = root.widgetWidth; root.dragOverrideH = root.widgetHeight; } onPositionChanged: mouse => { if (!pressed) return; const currentPos = root.useGhostPreview ? Qt.point(mouse.x, mouse.y) : mapToGlobal(mouse.x, mouse.y); let newW = Math.max(root.minWidth, Math.min(startWidth + currentPos.x - startPos.x, root.screenWidth - root.widgetX)); let newH = Math.max(root.minHeight, Math.min(startHeight + currentPos.y - startPos.y, root.screenHeight - root.widgetY)); if (root.gridEnabled) { newW = Math.max(root.minWidth, root.snapToGrid(newW)); newH = Math.max(root.minHeight, root.snapToGrid(newH)); } 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); } if (root.useGhostPreview) { root.previewWidth = newW; root.previewHeight = newH; return; } root.dragOverrideW = newW; root.dragOverrideH = newH; } onReleased: { const finalW = root.useGhostPreview ? root.previewWidth : root.dragOverrideW; const finalH = root.useGhostPreview ? root.previewHeight : root.dragOverrideH; root.saveSize(finalW, finalH); root.clearDragOverrides(); } } } Loader { active: root.isInteracting && root.useGhostPreview sourceComponent: PanelWindow { id: ghostPreviewWindow screen: root.screen color: "transparent" anchors { left: true right: true top: true bottom: true } mask: Region {} WlrLayershell.namespace: "quickshell:desktop-widget-preview" WlrLayershell.layer: WlrLayer.Bottom WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: WlrKeyboardFocus.None Item { id: gridOverlay anchors.fill: parent visible: root.gridEnabled opacity: 0.3 Repeater { model: Math.ceil(root.screenWidth / root.gridSize) Rectangle { required property int index x: index * root.gridSize y: 0 width: 1 height: root.screenHeight color: Theme.primary } } Repeater { model: Math.ceil(root.screenHeight / root.gridSize) Rectangle { required property int index x: 0 y: index * root.gridSize width: root.screenWidth height: 1 color: Theme.primary } } } Rectangle { x: root.previewX y: root.previewY width: root.previewWidth height: root.previewHeight color: "transparent" border.color: Theme.primary border.width: 2 radius: Theme.cornerRadius Rectangle { width: 48 height: 48 anchors { right: parent.right bottom: parent.bottom } topLeftRadius: Theme.cornerRadius bottomRightRadius: Theme.cornerRadius color: Theme.primary opacity: resizeArea.pressed ? 1 : 0.6 } } } } Loader { active: root.isInteracting && root.gridEnabled && !root.useGhostPreview sourceComponent: PanelWindow { screen: root.screen color: "transparent" anchors { left: true right: true top: true bottom: true } mask: Region {} WlrLayershell.namespace: "quickshell:desktop-widget-grid" WlrLayershell.layer: root.overviewActive && (root.showOnOverview || root.showOnOverviewOnly) ? WlrLayer.Overlay : WlrLayer.Background WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: WlrKeyboardFocus.None Item { anchors.fill: parent opacity: 0.3 Repeater { model: Math.ceil(root.screenWidth / root.gridSize) Rectangle { required property int index x: index * root.gridSize y: 0 width: 1 height: root.screenHeight color: Theme.primary } } Repeater { model: Math.ceil(root.screenHeight / root.gridSize) Rectangle { required property int index x: 0 y: index * root.gridSize width: root.screenWidth height: 1 color: Theme.primary } } } } } Loader { active: root.isInteracting sourceComponent: PanelWindow { id: helperWindow screen: root.screen color: "transparent" WlrLayershell.namespace: "quickshell:desktop-widget-helper" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: WlrKeyboardFocus.None anchors { bottom: true left: true right: true } implicitHeight: 60 Rectangle { id: helperContent anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: Theme.spacingL width: helperRow.implicitWidth + Theme.spacingM * 2 height: 32 radius: Theme.cornerRadius color: Theme.surface Row { id: helperRow anchors.centerIn: parent spacing: Theme.spacingM height: parent.height DankIcon { name: "grid_on" size: 16 color: root.gridEnabled ? Theme.primary : Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } Text { text: root.gridEnabled ? I18n.tr("Grid: ON", "Widget grid snap status") : I18n.tr("Grid: OFF", "Widget grid snap status") font.pixelSize: Theme.fontSizeSmall font.family: Theme.fontFamily color: root.gridEnabled ? Theme.primary : Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } Rectangle { width: 1 height: 16 color: Theme.outline anchors.verticalCenter: parent.verticalCenter } Text { text: root.gridSize + "px" font.pixelSize: Theme.fontSizeSmall font.family: Theme.fontFamily color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } Rectangle { width: 1 height: 16 color: Theme.outline anchors.verticalCenter: parent.verticalCenter } Text { text: I18n.tr("G: grid • Z/X: size", "Widget grid keyboard hints") font.pixelSize: Theme.fontSizeSmall font.family: Theme.fontFamily font.italic: true color: Theme.surfaceText opacity: 0.7 anchors.verticalCenter: parent.verticalCenter } } } } } }