summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Modules/Plugins
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modules/Plugins')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/BasePill.qml195
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ColorSetting.qml104
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/DesktopPluginComponent.qml67
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/DesktopPluginWrapper.qml758
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ListSetting.qml132
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ListSettingWithInput.qml256
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginComponent.qml333
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginControlCenterWrapper.qml51
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginPopout.qml83
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginSettings.qml311
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PopoutComponent.qml87
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/SelectionSetting.qml91
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/SliderSetting.qml81
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/StringSetting.qml84
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ToggleSetting.qml81
15 files changed, 2714 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/BasePill.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/BasePill.qml
new file mode 100644
index 0000000..7dea95a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/BasePill.qml
@@ -0,0 +1,195 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property var axis: null
+ property string section: "center"
+ property var popoutTarget: null
+ property var parentScreen: null
+ property real widgetThickness: 30
+ property real barThickness: 48
+ property real barSpacing: 4
+ property var barConfig: null
+ property var blurBarWindow: null
+ property alias content: contentLoader.sourceComponent
+ property bool isVerticalOrientation: axis?.isVertical ?? false
+ property bool isFirst: false
+ property bool isLast: false
+ property real sectionSpacing: 0
+ property bool enableBackgroundHover: true
+ property bool enableCursor: true
+ readonly property bool isMouseHovered: mouseArea.containsMouse
+ property bool isLeftBarEdge: false
+ property bool isRightBarEdge: false
+ property bool isTopBarEdge: false
+ property bool isBottomBarEdge: false
+ readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1
+ readonly property real horizontalPadding: (barConfig?.removeWidgetPadding ?? false) ? 0 : Theme.snap((barConfig?.widgetPadding ?? 12) * (widgetThickness / 30), dpr)
+ readonly property real visualWidth: Theme.snap(isVerticalOrientation ? widgetThickness : (contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0), dpr)
+ readonly property real visualHeight: Theme.snap(isVerticalOrientation ? (contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0) : widgetThickness, dpr)
+ readonly property alias visualContent: visualContent
+ readonly property real barEdgeExtension: 1000
+ readonly property real gapExtension: sectionSpacing
+ readonly property real leftMargin: !isVerticalOrientation ? (isLeftBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
+ readonly property real rightMargin: !isVerticalOrientation ? (isRightBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
+ readonly property real topMargin: isVerticalOrientation ? (isTopBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
+ readonly property real bottomMargin: isVerticalOrientation ? (isBottomBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
+
+ signal clicked
+ signal rightClicked(real rootX, real rootY)
+ signal wheel(var wheelEvent)
+
+ function triggerRipple(sourceItem, mouseX, mouseY) {
+ const pos = sourceItem.mapToItem(visualContent, mouseX, mouseY);
+ rippleLayer.trigger(pos.x, pos.y);
+ }
+
+ width: isVerticalOrientation ? barThickness : visualWidth
+ height: isVerticalOrientation ? visualHeight : barThickness
+
+ Item {
+ id: visualContent
+ width: root.visualWidth
+ height: root.visualHeight
+ anchors.centerIn: parent
+
+ Rectangle {
+ id: outline
+ anchors.centerIn: parent
+ width: {
+ const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
+ return parent.width + borderWidth * 2;
+ }
+ height: {
+ const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
+ return parent.height + borderWidth * 2;
+ }
+ radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
+ color: "transparent"
+ border.width: {
+ if (barConfig?.widgetOutlineEnabled ?? false) {
+ return barConfig?.widgetOutlineThickness ?? 1;
+ }
+ return 0;
+ }
+ border.color: {
+ if (!(barConfig?.widgetOutlineEnabled ?? false)) {
+ return "transparent";
+ }
+ const colorOption = barConfig?.widgetOutlineColor || "primary";
+ const opacity = barConfig?.widgetOutlineOpacity ?? 1.0;
+ switch (colorOption) {
+ case "surfaceText":
+ return Theme.withAlpha(Theme.surfaceText, opacity);
+ case "secondary":
+ return Theme.withAlpha(Theme.secondary, opacity);
+ case "primary":
+ return Theme.withAlpha(Theme.primary, opacity);
+ default:
+ return Theme.withAlpha(Theme.primary, opacity);
+ }
+ }
+ }
+
+ Rectangle {
+ id: background
+ anchors.fill: parent
+ radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
+ color: {
+ if (barConfig?.noBackground ?? false) {
+ return "transparent";
+ }
+
+ const rawTransparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
+ const isHovered = root.enableBackgroundHover && (mouseArea.containsMouse || (root.isHovered || false));
+ const transparency = isHovered ? Math.max(0.3, rawTransparency) : rawTransparency;
+ const baseColor = isHovered ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.widgetBaseBackgroundColor;
+
+ if (Theme.widgetBackgroundHasAlpha) {
+ return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
+ }
+ return Theme.withAlpha(baseColor, transparency);
+ }
+ }
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: background.radius
+ }
+
+ Loader {
+ id: contentLoader
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ z: -1
+ x: -root.leftMargin
+ y: -root.topMargin
+ width: root.width + root.leftMargin + root.rightMargin
+ height: root.height + root.topMargin + root.bottomMargin
+ hoverEnabled: true
+ cursorShape: root.enableCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onPressed: function (mouse) {
+ if (mouse.button === Qt.RightButton) {
+ const rPos = mouseArea.mapToItem(root, mouse.x, mouse.y);
+ root.rightClicked(rPos.x, rPos.y);
+ return;
+ }
+ const ripplePos = mouseArea.mapToItem(visualContent, mouse.x, mouse.y);
+ rippleLayer.trigger(ripplePos.x, ripplePos.y);
+ if (popoutTarget) {
+ // Ensure bar context is set first if supported
+ if (popoutTarget.setBarContext) {
+ const pos = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
+ const bottomGap = root.barConfig ? (root.barConfig.bottomGap !== undefined ? root.barConfig.bottomGap : 0) : 0;
+ popoutTarget.setBarContext(pos, bottomGap);
+ }
+
+ if (popoutTarget.setTriggerPosition) {
+ const globalPos = root.visualContent.mapToItem(null, 0, 0);
+ const currentScreen = parentScreen || Screen;
+ const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth, root.barSpacing, barPosition, root.barConfig);
+ popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, root.barSpacing, root.barConfig);
+ }
+ }
+ root.clicked();
+ }
+ onWheel: function (wheelEvent) {
+ wheelEvent.accepted = false;
+ root.wheel(wheelEvent);
+ }
+ }
+
+ property bool _blurRegistered: false
+ readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
+
+ on_ShouldBlurChanged: _updateBlurRegistration()
+
+ function _updateBlurRegistration() {
+ if (_shouldBlur && !_blurRegistered) {
+ blurBarWindow.registerBlurWidget(visualContent);
+ _blurRegistered = true;
+ } else if (!_shouldBlur && _blurRegistered) {
+ if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
+ blurBarWindow.unregisterBlurWidget(visualContent);
+ _blurRegistered = false;
+ }
+ }
+
+ Component.onCompleted: _updateBlurRegistration()
+ Component.onDestruction: {
+ if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
+ blurBarWindow.unregisterBlurWidget(visualContent);
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ColorSetting.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ColorSetting.qml
new file mode 100644
index 0000000..5ef3190
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ColorSetting.qml
@@ -0,0 +1,104 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Column {
+ id: root
+
+ required property string settingKey
+ required property string label
+ property string description: ""
+ property color defaultValue: Theme.primary
+ property color value: defaultValue
+
+ width: parent.width
+ spacing: Theme.spacingS
+
+ property bool isInitialized: false
+
+ function loadValue() {
+ const settings = findSettings();
+ if (settings && settings.pluginService) {
+ const loadedValue = settings.loadValue(settingKey, defaultValue);
+ value = loadedValue;
+ isInitialized = true;
+ }
+ }
+
+ Component.onCompleted: {
+ Qt.callLater(loadValue);
+ }
+
+ onValueChanged: {
+ if (!isInitialized)
+ return;
+ const settings = findSettings();
+ if (settings) {
+ settings.saveValue(settingKey, value);
+ }
+ }
+
+ function findSettings() {
+ let item = parent;
+ while (item) {
+ if (item.saveValue !== undefined && item.loadValue !== undefined) {
+ return item;
+ }
+ item = item.parent;
+ }
+ return null;
+ }
+
+ StyledText {
+ text: root.label
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ visible: root.description !== ""
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: 100
+ height: 36
+ radius: Theme.cornerRadius
+ color: root.value
+ border.color: Theme.outlineStrong
+ border.width: 2
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (PopoutService && PopoutService.colorPickerModal) {
+ PopoutService.colorPickerModal.selectedColor = root.value;
+ PopoutService.colorPickerModal.pickerTitle = root.label;
+ PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
+ root.value = selectedColor;
+ };
+ PopoutService.colorPickerModal.show();
+ }
+ }
+ }
+ }
+
+ StyledText {
+ text: root.value.toString()
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/DesktopPluginComponent.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/DesktopPluginComponent.qml
new file mode 100644
index 0000000..1fd7ab8
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/DesktopPluginComponent.qml
@@ -0,0 +1,67 @@
+import QtQuick
+import qs.Common
+
+Item {
+ id: root
+
+ property var pluginService: null
+ property string pluginId: ""
+ property string instanceId: ""
+ property var instanceData: null
+
+ property real widgetWidth: 200
+ property real widgetHeight: 200
+ property real minWidth: 100
+ property real minHeight: 100
+
+ readonly property bool isInstance: instanceId !== "" && instanceData !== null
+ readonly property var instanceConfig: instanceData?.config ?? {}
+
+ property var pluginData: isInstance ? instanceConfig : _globalPluginData
+ property var _globalPluginData: ({})
+
+ Component.onCompleted: loadPluginData()
+ onPluginServiceChanged: loadPluginData()
+ onPluginIdChanged: loadPluginData()
+ onInstanceDataChanged: {
+ if (isInstance)
+ Qt.callLater(() => {
+ pluginData = instanceConfig;
+ });
+ }
+
+ Connections {
+ target: pluginService
+ enabled: pluginService !== null
+
+ function onPluginDataChanged(changedPluginId) {
+ if (changedPluginId !== pluginId)
+ return;
+ loadPluginData();
+ }
+ }
+
+ function loadPluginData() {
+ if (!pluginService || !pluginId) {
+ _globalPluginData = {};
+ return;
+ }
+ if (isInstance) {
+ pluginData = instanceConfig;
+ return;
+ }
+ _globalPluginData = 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);
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/DesktopPluginWrapper.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/DesktopPluginWrapper.qml
new file mode 100644
index 0000000..c7a5c76
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/DesktopPluginWrapper.qml
@@ -0,0 +1,758 @@
+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: "dms: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
+ active: root.widgetEnabled && root.activeComponent !== null
+ 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: "dms: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: "dms: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: "dms: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
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ListSetting.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ListSetting.qml
new file mode 100644
index 0000000..5f636f5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ListSetting.qml
@@ -0,0 +1,132 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Column {
+ id: root
+
+ required property string settingKey
+ required property string label
+ property string description: ""
+ property var defaultValue: []
+ property var items: defaultValue
+ property Component delegate: null
+
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Component.onCompleted: {
+ const settings = findSettings()
+ if (settings) {
+ items = settings.loadValue(settingKey, defaultValue)
+ }
+ }
+
+ onItemsChanged: {
+ const settings = findSettings()
+ if (settings) {
+ settings.saveValue(settingKey, items)
+ }
+ }
+
+ function findSettings() {
+ let item = parent
+ while (item) {
+ if (item.saveValue !== undefined && item.loadValue !== undefined) {
+ return item
+ }
+ item = item.parent
+ }
+ return null
+ }
+
+ function addItem(item) {
+ items = items.concat([item])
+ }
+
+ function removeItem(index) {
+ const newItems = items.slice()
+ newItems.splice(index, 1)
+ items = newItems
+ }
+
+ StyledText {
+ text: root.label
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ visible: root.description !== ""
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.items
+ delegate: root.delegate ? root.delegate : defaultDelegate
+ }
+
+ StyledText {
+ text: I18n.tr("No items added yet")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: root.items.length === 0
+ }
+ }
+
+ Component {
+ id: defaultDelegate
+ StyledRect {
+ width: parent.width
+ height: 40
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.width: 0
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelData
+ color: Theme.surfaceText
+ }
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ width: 60
+ height: 28
+ color: removeArea.containsMouse ? Theme.errorHover : Theme.error
+ radius: Theme.cornerRadius
+
+ StyledText {
+ anchors.centerIn: parent
+ text: I18n.tr("Remove")
+ color: Theme.errorText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: removeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.removeItem(index)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ListSettingWithInput.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ListSettingWithInput.qml
new file mode 100644
index 0000000..8da4fb6
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ListSettingWithInput.qml
@@ -0,0 +1,256 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Column {
+ id: root
+
+ required property string settingKey
+ required property string label
+ property string description: ""
+ property var fields: []
+ property var defaultValue: []
+ property var items: defaultValue
+
+ width: parent.width
+ spacing: Theme.spacingM
+
+ property bool isLoading: false
+
+ Component.onCompleted: {
+ loadValue()
+ }
+
+ function loadValue() {
+ const settings = findSettings()
+ if (settings) {
+ isLoading = true
+ items = settings.loadValue(settingKey, defaultValue)
+ isLoading = false
+ }
+ }
+
+ onItemsChanged: {
+ if (isLoading) {
+ return
+ }
+ const settings = findSettings()
+ if (settings) {
+ settings.saveValue(settingKey, items)
+ }
+ }
+
+ function findSettings() {
+ let item = parent
+ while (item) {
+ if (item.saveValue !== undefined && item.loadValue !== undefined) {
+ return item
+ }
+ item = item.parent
+ }
+ return null
+ }
+
+ function addItem(item) {
+ items = items.concat([item])
+ }
+
+ function removeItem(index) {
+ const newItems = items.slice()
+ newItems.splice(index, 1)
+ items = newItems
+ }
+
+ StyledText {
+ text: root.label
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ visible: root.description !== ""
+ }
+
+ Flow {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.fields
+
+ StyledText {
+ text: modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ width: modelData.width || 200
+ }
+ }
+ }
+
+ Flow {
+ id: inputRow
+ width: parent.width
+ spacing: Theme.spacingS
+
+ property var inputFields: []
+
+ Repeater {
+ id: inputRepeater
+ model: root.fields
+
+ DankTextField {
+ width: modelData.width || 200
+ placeholderText: modelData.placeholder || ""
+
+ Component.onCompleted: {
+ inputRow.inputFields.push(this)
+ }
+
+ Keys.onReturnPressed: {
+ addButton.clicked()
+ }
+ }
+ }
+
+ DankButton {
+ id: addButton
+ width: 50
+ height: 36
+ text: I18n.tr("Add")
+
+ onClicked: {
+ let newItem = {}
+ let hasValue = false
+
+ for (let i = 0; i < root.fields.length; i++) {
+ const field = root.fields[i]
+ const input = inputRow.inputFields[i]
+ const value = input.text.trim()
+
+ if (value !== "") {
+ hasValue = true
+ }
+
+ if (field.required && value === "") {
+ return
+ }
+
+ newItem[field.id] = value || (field.default || "")
+ }
+
+ if (hasValue) {
+ root.addItem(newItem)
+ for (let i = 0; i < inputRow.inputFields.length; i++) {
+ inputRow.inputFields[i].text = ""
+ }
+ if (inputRow.inputFields.length > 0) {
+ inputRow.inputFields[0].forceActiveFocus()
+ }
+ }
+ }
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Current Items")
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ visible: root.items.length > 0
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.items
+
+ StyledRect {
+ width: parent.width
+ height: 40
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.width: 0
+
+ required property int index
+ required property var modelData
+
+ Row {
+ id: itemRow
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ property var itemData: parent.modelData
+
+ Repeater {
+ model: root.fields
+
+ StyledText {
+ required property int index
+ required property var modelData
+
+ text: {
+ const field = modelData
+ const item = itemRow.itemData
+ if (!field || !field.id || !item) {
+ return ""
+ }
+ const value = item[field.id]
+ return value || ""
+ }
+ color: Theme.surfaceText
+ font.pixelSize: Theme.fontSizeMedium
+ width: modelData ? (modelData.width || 200) : 200
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ width: 60
+ height: 28
+ color: removeArea.containsMouse ? Theme.errorHover : Theme.error
+ radius: Theme.cornerRadius
+
+ StyledText {
+ anchors.centerIn: parent
+ text: I18n.tr("Remove")
+ color: Theme.onError
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: removeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.removeItem(index)
+ }
+ }
+ }
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("No items added yet")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: root.items.length === 0
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginComponent.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginComponent.qml
new file mode 100644
index 0000000..85dd32d
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginComponent.qml
@@ -0,0 +1,333 @@
+import QtQuick
+import Quickshell.Io
+import qs.Common
+
+Item {
+ id: root
+
+ property string layerNamespacePlugin: "plugin"
+
+ property var axis: null
+ property string section: "center"
+ property var parentScreen: null
+ property real widgetThickness: 30
+ property real barThickness: 48
+ property real barSpacing: 4
+ property var barConfig: null
+ property var blurBarWindow: null
+ property string pluginId: ""
+ property var pluginService: null
+
+ property string visibilityCommand: ""
+ property int visibilityInterval: 0
+ property bool conditionVisible: true
+ property bool _visibilityOverride: false
+ property bool _visibilityOverrideValue: true
+
+ readonly property bool effectiveVisible: {
+ if (_visibilityOverride)
+ return _visibilityOverrideValue;
+ if (!visibilityCommand)
+ return true;
+ return conditionVisible;
+ }
+
+ property Component horizontalBarPill: null
+ property Component verticalBarPill: null
+ property Component popoutContent: null
+ property real popoutWidth: 400
+ property real popoutHeight: 0
+ property var pillClickAction: null
+ property var pillRightClickAction: null
+
+ property Component controlCenterWidget: null
+ property string ccWidgetIcon: ""
+ property string ccWidgetPrimaryText: ""
+ property string ccWidgetSecondaryText: ""
+ property bool ccWidgetIsActive: false
+ property bool ccWidgetIsToggle: true
+ property Component ccDetailContent: null
+ property real ccDetailHeight: 250
+
+ signal ccWidgetToggled
+ signal ccWidgetExpanded
+
+ property var pluginData: ({})
+ property var variants: []
+
+ readonly property bool isVertical: axis?.isVertical ?? false
+ readonly property bool hasHorizontalPill: horizontalBarPill !== null
+ readonly property bool hasVerticalPill: verticalBarPill !== null
+ readonly property bool hasPopout: popoutContent !== null
+
+ readonly property int iconSize: Theme.barIconSize(barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+ readonly property int iconSizeLarge: Theme.barIconSize(barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
+
+ Component.onCompleted: {
+ loadPluginData();
+ if (visibilityCommand)
+ Qt.callLater(checkVisibility);
+ }
+
+ onPluginServiceChanged: {
+ loadPluginData();
+ }
+
+ onPluginIdChanged: {
+ loadPluginData();
+ }
+
+ Connections {
+ target: pluginService
+ function onPluginDataChanged(changedPluginId) {
+ if (changedPluginId === pluginId) {
+ loadPluginData();
+ }
+ }
+ }
+
+ function loadPluginData() {
+ if (!pluginService || !pluginId) {
+ pluginData = {};
+ variants = [];
+ return;
+ }
+ pluginData = SettingsData.getPluginSettingsForPlugin(pluginId);
+ variants = pluginService.getPluginVariants(pluginId);
+ }
+
+ function checkVisibility() {
+ if (!visibilityCommand) {
+ conditionVisible = true;
+ return;
+ }
+ visibilityProcess.running = true;
+ }
+
+ function setVisibilityOverride(visible) {
+ _visibilityOverride = true;
+ _visibilityOverrideValue = visible;
+ }
+
+ function clearVisibilityOverride() {
+ _visibilityOverride = false;
+ if (visibilityCommand)
+ checkVisibility();
+ }
+
+ onVisibilityCommandChanged: {
+ if (visibilityCommand)
+ Qt.callLater(checkVisibility);
+ else
+ conditionVisible = true;
+ }
+
+ onVisibilityIntervalChanged: {
+ if (visibilityInterval > 0 && visibilityCommand) {
+ visibilityTimer.restart();
+ } else {
+ visibilityTimer.stop();
+ }
+ }
+
+ Timer {
+ id: visibilityTimer
+ interval: root.visibilityInterval * 1000
+ repeat: true
+ running: root.visibilityInterval > 0 && root.visibilityCommand !== ""
+ onTriggered: root.checkVisibility()
+ }
+
+ Process {
+ id: visibilityProcess
+ command: ["sh", "-c", root.visibilityCommand]
+ running: false
+ onExited: (exitCode, exitStatus) => {
+ root.conditionVisible = (exitCode === 0);
+ }
+ }
+
+ function createVariant(variantName, variantConfig) {
+ if (!pluginService || !pluginId) {
+ return null;
+ }
+ return pluginService.createPluginVariant(pluginId, variantName, variantConfig);
+ }
+
+ function removeVariant(variantId) {
+ if (!pluginService || !pluginId) {
+ return;
+ }
+ pluginService.removePluginVariant(pluginId, variantId);
+ }
+
+ function updateVariant(variantId, variantConfig) {
+ if (!pluginService || !pluginId) {
+ return;
+ }
+ pluginService.updatePluginVariant(pluginId, variantId, variantConfig);
+ }
+
+ width: isVertical ? (hasVerticalPill ? verticalPill.width : 0) : (hasHorizontalPill ? horizontalPill.width : 0)
+ height: isVertical ? (hasVerticalPill ? verticalPill.height : 0) : (hasHorizontalPill ? horizontalPill.height : 0)
+
+ BasePill {
+ id: horizontalPill
+ visible: !isVertical && hasHorizontalPill
+ opacity: root.effectiveVisible ? 1 : 0
+ axis: root.axis
+ section: root.section
+ popoutTarget: hasPopout ? pluginPopout : null
+ parentScreen: root.parentScreen
+ widgetThickness: root.widgetThickness
+ barThickness: root.barThickness
+ barSpacing: root.barSpacing
+ barConfig: root.barConfig
+ blurBarWindow: root.blurBarWindow
+ content: root.horizontalBarPill
+
+ states: State {
+ name: "hidden"
+ when: !root.effectiveVisible
+ PropertyChanges {
+ target: horizontalPill
+ width: 0
+ }
+ }
+
+ transitions: Transition {
+ NumberAnimation {
+ properties: "width,opacity"
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ onClicked: {
+ if (pillClickAction) {
+ if (pillClickAction.length === 0) {
+ pillClickAction();
+ } else {
+ const globalPos = mapToItem(null, 0, 0);
+ const currentScreen = parentScreen || Screen;
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width);
+ pillClickAction(pos.x, pos.y, pos.width, section, currentScreen);
+ }
+ } else if (hasPopout) {
+ pluginPopout.toggle();
+ }
+ }
+ onRightClicked: {
+ if (pillRightClickAction) {
+ if (pillRightClickAction.length === 0) {
+ pillRightClickAction();
+ } else {
+ const globalPos = mapToItem(null, 0, 0);
+ const currentScreen = parentScreen || Screen;
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width);
+ pillRightClickAction(pos.x, pos.y, pos.width, section, currentScreen);
+ }
+ }
+ }
+ }
+
+ BasePill {
+ id: verticalPill
+ visible: isVertical && hasVerticalPill
+ opacity: root.effectiveVisible ? 1 : 0
+ axis: root.axis
+ section: root.section
+ popoutTarget: hasPopout ? pluginPopout : null
+ parentScreen: root.parentScreen
+ widgetThickness: root.widgetThickness
+ barThickness: root.barThickness
+ barSpacing: root.barSpacing
+ barConfig: root.barConfig
+ blurBarWindow: root.blurBarWindow
+ content: root.verticalBarPill
+ isVerticalOrientation: true
+
+ states: State {
+ name: "hidden"
+ when: !root.effectiveVisible
+ PropertyChanges {
+ target: verticalPill
+ height: 0
+ }
+ }
+
+ transitions: Transition {
+ NumberAnimation {
+ properties: "height,opacity"
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ onClicked: {
+ if (pillClickAction) {
+ if (pillClickAction.length === 0) {
+ pillClickAction();
+ } else {
+ const globalPos = mapToItem(null, 0, 0);
+ const currentScreen = parentScreen || Screen;
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width);
+ pillClickAction(pos.x, pos.y, pos.width, section, currentScreen);
+ }
+ } else if (hasPopout) {
+ pluginPopout.toggle();
+ }
+ }
+ onRightClicked: {
+ if (pillRightClickAction) {
+ if (pillRightClickAction.length === 0) {
+ pillRightClickAction();
+ } else {
+ const globalPos = mapToItem(null, 0, 0);
+ const currentScreen = parentScreen || Screen;
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width);
+ pillRightClickAction(pos.x, pos.y, pos.width, section, currentScreen);
+ }
+ }
+ }
+ }
+
+ function closePopout() {
+ if (pluginPopout) {
+ pluginPopout.close();
+ }
+ }
+
+ function triggerPopout() {
+ if (pillClickAction) {
+ if (pillClickAction.length === 0) {
+ pillClickAction();
+ return;
+ }
+ const pill = isVertical ? verticalPill : horizontalPill;
+ const globalPos = pill.mapToItem(null, 0, 0);
+ const currentScreen = parentScreen || Screen;
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, pill.width);
+ pillClickAction(pos.x, pos.y, pos.width, section, currentScreen);
+ return;
+ }
+ if (!hasPopout)
+ return;
+
+ const pill = isVertical ? verticalPill : horizontalPill;
+ const globalPos = pill.visualContent.mapToItem(null, 0, 0);
+ const currentScreen = parentScreen || Screen;
+ const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1));
+ const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, pill.visualWidth, barSpacing, barPosition, barConfig);
+
+ pluginPopout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, barSpacing, barConfig);
+ pluginPopout.toggle();
+ }
+
+ PluginPopout {
+ id: pluginPopout
+ contentWidth: root.popoutWidth
+ contentHeight: root.popoutHeight
+ pluginContent: root.popoutContent
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginControlCenterWrapper.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginControlCenterWrapper.qml
new file mode 100644
index 0000000..5d40826
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginControlCenterWrapper.qml
@@ -0,0 +1,51 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property string pluginId: ""
+ property var pluginInstance: null
+ property bool isCompoundPill: false
+ property bool isSmallToggle: false
+
+ readonly property bool hasDetail: pluginInstance?.ccDetailContent !== null
+ readonly property string iconName: pluginInstance?.ccWidgetIcon || "extension"
+ readonly property string primaryText: pluginInstance?.ccWidgetPrimaryText || "Plugin"
+ readonly property string secondaryText: pluginInstance?.ccWidgetSecondaryText || ""
+ readonly property bool isActive: pluginInstance?.ccWidgetIsActive || false
+ readonly property Component detailContent: pluginInstance?.ccDetailContent || null
+ readonly property real detailHeight: pluginInstance?.ccDetailHeight || 250
+
+ signal toggled()
+ signal expanded()
+
+ Component.onCompleted: {
+ if (pluginInstance) {
+ pluginInstance.ccWidgetToggled.connect(handleToggled)
+ pluginInstance.ccWidgetExpanded.connect(handleExpanded)
+ }
+ }
+
+ function handleToggled() {
+ toggled()
+ }
+
+ function handleExpanded() {
+ expanded()
+ }
+
+ function invokeToggle() {
+ if (pluginInstance) {
+ pluginInstance.ccWidgetToggled()
+ }
+ }
+
+ function invokeExpand() {
+ if (pluginInstance) {
+ pluginInstance.ccWidgetExpanded()
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginPopout.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginPopout.qml
new file mode 100644
index 0000000..acd24b9
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginPopout.qml
@@ -0,0 +1,83 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+DankPopout {
+ id: root
+
+ layerNamespace: "dms:plugins:" + layerNamespacePlugin
+
+ property var triggerScreen: null
+ property Component pluginContent: null
+ property real contentWidth: 400
+ property real contentHeight: 0
+
+ popupWidth: contentWidth
+ popupHeight: contentHeight
+ screen: triggerScreen
+ shouldBeVisible: false
+
+ onBackgroundClicked: close()
+
+ content: Component {
+ Rectangle {
+ id: popoutContainer
+
+ implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
+ color: "transparent"
+ focus: true
+
+ Component.onCompleted: {
+ if (root.shouldBeVisible) {
+ forceActiveFocus();
+ }
+ }
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Escape) {
+ root.close();
+ event.accepted = true;
+ }
+ }
+
+ Connections {
+ target: root
+ function onShouldBeVisibleChanged() {
+ if (root.shouldBeVisible) {
+ Qt.callLater(() => {
+ popoutContainer.forceActiveFocus();
+ });
+ }
+ }
+ }
+
+ Column {
+ id: popoutColumn
+ width: parent.width - Theme.spacingS * 2
+ x: Theme.spacingS
+ y: Theme.spacingS
+ spacing: Theme.spacingS
+
+ Loader {
+ id: popoutContentLoader
+ width: parent.width
+ sourceComponent: root.pluginContent
+
+ onLoaded: {
+ if (item && "closePopout" in item) {
+ item.closePopout = function () {
+ root.close();
+ };
+ }
+ if (item && "parentPopout" in item) {
+ item.parentPopout = root;
+ }
+ if (item) {
+ root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginSettings.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginSettings.qml
new file mode 100644
index 0000000..2ba5d05
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PluginSettings.qml
@@ -0,0 +1,311 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: root
+
+ required property string pluginId
+ property var pluginService: null
+ default property list<QtObject> content
+
+ signal settingChanged
+
+ property var variants: []
+ property alias variantsModel: variantsListModel
+
+ implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
+ height: implicitHeight
+
+ readonly property bool isDesktopPlugin: {
+ if (!pluginService?.availablePlugins || !pluginId)
+ return false;
+ const plugin = pluginService.availablePlugins[pluginId];
+ return plugin?.type === "desktop";
+ }
+
+ readonly property bool hasPermission: {
+ if (!pluginService?.availablePlugins || !pluginId)
+ return true;
+ const plugin = pluginService.availablePlugins[pluginId];
+ if (!plugin)
+ return true;
+ const permissions = Array.isArray(plugin.permissions) ? plugin.permissions : [];
+ return permissions.indexOf("settings_write") !== -1;
+ }
+
+ Component.onCompleted: {
+ loadVariants();
+ }
+
+ onPluginServiceChanged: {
+ if (pluginService) {
+ loadVariants();
+ for (let i = 0; i < content.length; i++) {
+ const child = content[i];
+ if (child.loadValue) {
+ child.loadValue();
+ }
+ }
+ }
+ }
+
+ onContentChanged: {
+ for (let i = 0; i < content.length; i++) {
+ const item = content[i];
+ if (item instanceof Item) {
+ item.parent = settingsColumn;
+ }
+ }
+ }
+
+ Connections {
+ target: pluginService
+ enabled: pluginService !== null
+
+ function onPluginDataChanged(changedPluginId) {
+ if (changedPluginId === pluginId) {
+ loadVariants();
+ reloadChildValues();
+ }
+ }
+ }
+
+ function reloadChildValues() {
+ for (let i = 0; i < content.length; i++) {
+ const child = content[i];
+ if (child.loadValue) {
+ child.loadValue();
+ }
+ }
+ }
+
+ function loadVariants() {
+ if (!pluginService?.getPluginVariants || !pluginId) {
+ variants = [];
+ return;
+ }
+ variants = pluginService.getPluginVariants(pluginId);
+ syncVariantsToModel();
+ }
+
+ function syncVariantsToModel() {
+ variantsListModel.clear();
+ for (let i = 0; i < variants.length; i++) {
+ variantsListModel.append(variants[i]);
+ }
+ }
+
+ onVariantsChanged: {
+ syncVariantsToModel();
+ }
+
+ ListModel {
+ id: variantsListModel
+ }
+
+ function createVariant(variantName, variantConfig) {
+ if (!pluginService?.createPluginVariant || !pluginId) {
+ return null;
+ }
+ return pluginService.createPluginVariant(pluginId, variantName, variantConfig);
+ }
+
+ function removeVariant(variantId) {
+ if (!pluginService?.removePluginVariant || !pluginId) {
+ return;
+ }
+ pluginService.removePluginVariant(pluginId, variantId);
+ }
+
+ function updateVariant(variantId, variantConfig) {
+ if (!pluginService?.updatePluginVariant || !pluginId) {
+ return;
+ }
+ pluginService.updatePluginVariant(pluginId, variantId, variantConfig);
+ }
+
+ function saveValue(key, value) {
+ if (!pluginService) {
+ return;
+ }
+ if (!hasPermission) {
+ console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission");
+ return;
+ }
+ if (pluginService.savePluginData) {
+ pluginService.savePluginData(pluginId, key, value);
+ settingChanged();
+ }
+ }
+
+ function loadValue(key, defaultValue) {
+ if (pluginService && pluginService.loadPluginData) {
+ return pluginService.loadPluginData(pluginId, key, defaultValue);
+ }
+ return defaultValue;
+ }
+
+ function saveState(key, value) {
+ if (!pluginService)
+ return;
+ if (pluginService.savePluginState)
+ pluginService.savePluginState(pluginId, key, value);
+ }
+
+ function loadState(key, defaultValue) {
+ if (pluginService && pluginService.loadPluginState)
+ return pluginService.loadPluginState(pluginId, key, defaultValue);
+ return defaultValue;
+ }
+
+ function clearState() {
+ if (pluginService && pluginService.clearPluginState)
+ pluginService.clearPluginState(pluginId);
+ }
+
+ function findFlickable(item) {
+ var current = item?.parent;
+ while (current) {
+ if (current.contentY !== undefined && current.contentHeight !== undefined) {
+ return current;
+ }
+ current = current.parent;
+ }
+ return null;
+ }
+
+ function ensureItemVisible(item) {
+ 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 itemRelativeY = itemGlobalY - flickableGlobalY;
+ var viewportTop = 0;
+ var viewportBottom = viewportHeight;
+
+ if (itemRelativeY < viewportTop) {
+ 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);
+ }
+ }
+
+ StyledText {
+ id: errorText
+ visible: pluginService && !root.hasPermission
+ anchors.fill: parent
+ text: I18n.tr("This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json")
+ color: Theme.error
+ font.pixelSize: Theme.fontSizeMedium
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ Column {
+ id: settingsColumn
+ 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);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PopoutComponent.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PopoutComponent.qml
new file mode 100644
index 0000000..7ccf637
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/PopoutComponent.qml
@@ -0,0 +1,87 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Column {
+ id: root
+
+ property string headerText: ""
+ property string detailsText: ""
+ property bool showCloseButton: false
+ property var closePopout: null
+ property var parentPopout: null
+ property alias headerActions: headerActionsLoader.sourceComponent
+
+ readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0
+ readonly property int detailsHeight: popoutDetails.visible ? popoutDetails.implicitHeight : 0
+
+ spacing: 0
+
+ Item {
+ id: popoutHeader
+ width: parent.width
+ height: 40
+ visible: headerText.length > 0
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.headerText
+ font.pixelSize: Theme.fontSizeLarge + 4
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ Loader {
+ id: headerActionsLoader
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Rectangle {
+ id: closeButton
+ width: 32
+ height: 32
+ radius: 16
+ color: closeArea.containsMouse ? Theme.errorHover : "transparent"
+ visible: root.showCloseButton
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "close"
+ size: Theme.iconSize - 4
+ color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
+ }
+
+ MouseArea {
+ id: closeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: {
+ if (root.closePopout) {
+ root.closePopout();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ StyledText {
+ id: popoutDetails
+ width: parent.width
+ leftPadding: Theme.spacingS
+ bottomPadding: Theme.spacingS
+ text: root.detailsText
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ visible: detailsText.length > 0
+ wrapMode: Text.WordWrap
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/SelectionSetting.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/SelectionSetting.qml
new file mode 100644
index 0000000..0f8fac0
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/SelectionSetting.qml
@@ -0,0 +1,91 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Column {
+ id: root
+
+ required property string settingKey
+ required property string label
+ property string description: ""
+ required property var options
+ property string defaultValue: ""
+ property string value: defaultValue
+
+ width: parent.width
+ spacing: Theme.spacingS
+
+ function loadValue() {
+ const settings = findSettings()
+ if (settings && settings.pluginService) {
+ value = settings.loadValue(settingKey, defaultValue)
+ }
+ }
+
+ Component.onCompleted: {
+ loadValue()
+ }
+
+ readonly property var optionLabels: {
+ const labels = []
+ for (let i = 0; i < options.length; i++) {
+ labels.push(options[i].label || options[i])
+ }
+ return labels
+ }
+
+ readonly property var valueToLabel: {
+ const map = {}
+ for (let i = 0; i < options.length; i++) {
+ const opt = options[i]
+ if (typeof opt === 'object') {
+ map[opt.value] = opt.label
+ } else {
+ map[opt] = opt
+ }
+ }
+ return map
+ }
+
+ readonly property var labelToValue: {
+ const map = {}
+ for (let i = 0; i < options.length; i++) {
+ const opt = options[i]
+ if (typeof opt === 'object') {
+ map[opt.label] = opt.value
+ } else {
+ map[opt] = opt
+ }
+ }
+ return map
+ }
+
+ onValueChanged: {
+ const settings = findSettings()
+ if (settings) {
+ settings.saveValue(settingKey, value)
+ }
+ }
+
+ function findSettings() {
+ let item = parent
+ while (item) {
+ if (item.saveValue !== undefined && item.loadValue !== undefined) {
+ return item
+ }
+ item = item.parent
+ }
+ return null
+ }
+
+ DankDropdown {
+ width: parent.width
+ text: root.label
+ description: root.description
+ currentValue: root.valueToLabel[root.value] || root.value
+ options: root.optionLabels
+ onValueChanged: newValue => {
+ root.value = root.labelToValue[newValue] || newValue
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/SliderSetting.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/SliderSetting.qml
new file mode 100644
index 0000000..f540fd4
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/SliderSetting.qml
@@ -0,0 +1,81 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Column {
+ id: root
+
+ required property string settingKey
+ required property string label
+ property string description: ""
+ property int defaultValue: 0
+ property int value: defaultValue
+ property int minimum: 0
+ property int maximum: 100
+ property string leftIcon: ""
+ property string rightIcon: ""
+ property string unit: ""
+
+ width: parent.width
+ spacing: Theme.spacingS
+
+ function loadValue() {
+ const settings = findSettings()
+ if (settings && settings.pluginService) {
+ value = settings.loadValue(settingKey, defaultValue)
+ }
+ }
+
+ Component.onCompleted: {
+ loadValue()
+ }
+
+ onValueChanged: {
+ const settings = findSettings()
+ if (settings) {
+ settings.saveValue(settingKey, value)
+ }
+ }
+
+ function findSettings() {
+ let item = parent
+ while (item) {
+ if (item.saveValue !== undefined && item.loadValue !== undefined) {
+ return item
+ }
+ item = item.parent
+ }
+ return null
+ }
+
+ StyledText {
+ text: root.label
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ visible: root.description !== ""
+ }
+
+ DankSlider {
+ width: parent.width
+ value: root.value
+ minimum: root.minimum
+ maximum: root.maximum
+ leftIcon: root.leftIcon
+ rightIcon: root.rightIcon
+ unit: root.unit
+ wheelEnabled: false
+ thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
+ onSliderValueChanged: newValue => {
+ root.value = newValue
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/StringSetting.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/StringSetting.qml
new file mode 100644
index 0000000..849807b
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/StringSetting.qml
@@ -0,0 +1,84 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Column {
+ id: root
+
+ required property string settingKey
+ required property string label
+ property string description: ""
+ property string placeholder: ""
+ property string defaultValue: ""
+ property string value: defaultValue
+
+ width: parent.width
+ spacing: Theme.spacingS
+
+ property bool isInitialized: false
+
+ function loadValue() {
+ const settings = findSettings();
+ if (settings && settings.pluginService) {
+ const loadedValue = settings.loadValue(settingKey, defaultValue);
+ if (textField.activeFocus && isInitialized)
+ return;
+ value = loadedValue;
+ textField.text = loadedValue;
+ isInitialized = true;
+ }
+ }
+
+ Component.onCompleted: {
+ Qt.callLater(loadValue);
+ }
+
+ function commit() {
+ if (!isInitialized)
+ return;
+ if (textField.text === value)
+ return;
+ value = textField.text;
+ const settings = findSettings();
+ if (settings)
+ settings.saveValue(settingKey, value);
+ }
+
+ function findSettings() {
+ let item = parent;
+ while (item) {
+ if (item.saveValue !== undefined && item.loadValue !== undefined) {
+ return item;
+ }
+ item = item.parent;
+ }
+ return null;
+ }
+
+ StyledText {
+ text: root.label
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ visible: root.description !== ""
+ }
+
+ DankTextField {
+ id: textField
+ width: parent.width
+ placeholderText: root.placeholder
+ onEditingFinished: root.commit()
+ onActiveFocusChanged: {
+ if (!activeFocus)
+ root.commit();
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ToggleSetting.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ToggleSetting.qml
new file mode 100644
index 0000000..3a6eae5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/Plugins/ToggleSetting.qml
@@ -0,0 +1,81 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Row {
+ id: root
+
+ required property string settingKey
+ required property string label
+ property string description: ""
+ property bool defaultValue: false
+ property bool value: defaultValue
+
+ width: parent.width
+ spacing: Theme.spacingM
+
+ property bool isInitialized: false
+
+ function loadValue() {
+ const settings = findSettings()
+ if (settings && settings.pluginService) {
+ const loadedValue = settings.loadValue(settingKey, defaultValue)
+ value = loadedValue
+ isInitialized = true
+ }
+ }
+
+ Component.onCompleted: {
+ Qt.callLater(loadValue)
+ }
+
+ onValueChanged: {
+ if (!isInitialized) return
+ const settings = findSettings()
+ if (settings) {
+ settings.saveValue(settingKey, value)
+ }
+ }
+
+ function findSettings() {
+ let item = parent
+ while (item) {
+ if (item.saveValue !== undefined && item.loadValue !== undefined) {
+ return item
+ }
+ item = item.parent
+ }
+ return null
+ }
+
+ Column {
+ width: parent.width - toggle.width - Theme.spacingM
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ text: root.label
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ visible: root.description !== ""
+ }
+ }
+
+ DankToggle {
+ id: toggle
+ anchors.verticalCenter: parent.verticalCenter
+ checked: root.value
+ onToggled: isChecked => {
+ root.value = isChecked
+ }
+ }
+}