1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-09 23:32:10 -04:00

desktop widgets: sync position across screens option, clickthrough

option, grouping in settings, repositioning, new IPCs for control
fixes #1300
fixes #1301
This commit is contained in:
bbedward
2026-01-12 15:31:34 -05:00
parent 59893b7f44
commit dcfb947c36
20 changed files with 1935 additions and 374 deletions

View File

@@ -26,6 +26,8 @@ Item {
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
@@ -83,6 +85,7 @@ Item {
}
}
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
@@ -96,53 +99,114 @@ Item {
readonly property bool hasSavedPosition: {
if (isInstance)
return instanceData?.positions?.[screenKey]?.x !== undefined;
return instanceData?.positions?.[positionKey]?.x !== undefined;
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopX_" + screenKey, null) !== null;
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "x", null) !== null;
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?.[screenKey]?.width !== undefined;
return instanceData?.positions?.[positionKey]?.width !== undefined;
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopWidth_" + screenKey, null) !== null;
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "width", null) !== null;
return pluginService.loadPluginData(pluginId, "desktopWidth_" + positionKey, null) !== null;
return SettingsData.getDesktopWidgetPosition(pluginId, positionKey, "width", null) !== null;
}
property real savedX: {
if (isInstance)
return instanceData?.positions?.[screenKey]?.x ?? (screenWidth / 2 - savedWidth / 2);
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopX_" + screenKey, screenWidth / 2 - savedWidth / 2);
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "x", screenWidth / 2 - savedWidth / 2);
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)
return instanceData?.positions?.[screenKey]?.y ?? (screenHeight / 2 - savedHeight / 2);
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopY_" + screenKey, screenHeight / 2 - savedHeight / 2);
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "y", screenHeight / 2 - savedHeight / 2);
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)
return instanceData?.positions?.[screenKey]?.width ?? 280;
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopWidth_" + screenKey, 200);
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "width", 280);
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)
return instanceData?.positions?.[screenKey]?.height ?? 180;
if (usePluginService)
return pluginService.loadPluginData(pluginId, "desktopHeight_" + screenKey, 200);
return SettingsData.getDesktopWidgetPosition(pluginId, screenKey, "height", 180);
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 widgetX: Math.max(0, Math.min(savedX, screenWidth - widgetWidth))
property real widgetY: Math.max(0, Math.min(savedY, screenHeight - widgetHeight))
property real widgetWidth: Math.max(minWidth, Math.min(savedWidth, screenWidth))
property real widgetHeight: Math.max(minHeight, Math.min(savedHeight, screenHeight))
property real 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
@@ -163,41 +227,45 @@ Item {
return Math.round(value / gridSize) * gridSize;
}
function savePosition() {
function savePosition(finalX, finalY) {
const xVal = syncPositionAcrossScreens ? finalX / screenWidth : finalX;
const yVal = syncPositionAcrossScreens ? finalY / screenHeight : finalY;
if (isInstance && instanceData) {
SettingsData.updateDesktopWidgetInstancePosition(instanceId, screenKey, {
x: root.widgetX,
y: root.widgetY
SettingsData.updateDesktopWidgetInstancePosition(instanceId, positionKey, {
x: xVal,
y: yVal
});
return;
}
if (usePluginService) {
pluginService.savePluginData(pluginId, "desktopX_" + screenKey, root.widgetX);
pluginService.savePluginData(pluginId, "desktopY_" + screenKey, root.widgetY);
pluginService.savePluginData(pluginId, "desktopX_" + positionKey, xVal);
pluginService.savePluginData(pluginId, "desktopY_" + positionKey, yVal);
return;
}
SettingsData.updateDesktopWidgetPosition(pluginId, screenKey, {
x: root.widgetX,
y: root.widgetY
SettingsData.updateDesktopWidgetPosition(pluginId, positionKey, {
x: xVal,
y: yVal
});
}
function saveSize() {
function saveSize(finalW, finalH) {
const sizeVal = forceSquare ? Math.max(finalW, finalH) : finalW;
const heightVal = forceSquare ? sizeVal : finalH;
if (isInstance && instanceData) {
SettingsData.updateDesktopWidgetInstancePosition(instanceId, screenKey, {
width: root.widgetWidth,
height: root.widgetHeight
SettingsData.updateDesktopWidgetInstancePosition(instanceId, positionKey, {
width: sizeVal,
height: heightVal
});
return;
}
if (usePluginService) {
pluginService.savePluginData(pluginId, "desktopWidth_" + screenKey, root.widgetWidth);
pluginService.savePluginData(pluginId, "desktopHeight_" + screenKey, root.widgetHeight);
pluginService.savePluginData(pluginId, "desktopWidth_" + positionKey, sizeVal);
pluginService.savePluginData(pluginId, "desktopHeight_" + positionKey, heightVal);
return;
}
SettingsData.updateDesktopWidgetPosition(pluginId, screenKey, {
width: root.widgetWidth,
height: root.widgetHeight
SettingsData.updateDesktopWidgetPosition(pluginId, positionKey, {
width: sizeVal,
height: heightVal
});
}
@@ -213,6 +281,12 @@ Item {
}
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)
@@ -315,12 +389,14 @@ Item {
if (!root.hasSavedSize) {
const defW = item.defaultWidth ?? item.widgetWidth ?? 280;
const defH = item.defaultHeight ?? item.widgetHeight ?? 180;
root.widgetWidth = Math.max(root.minWidth, Math.min(defW, root.screenWidth));
root.widgetHeight = Math.max(root.minHeight, Math.min(defH, root.screenHeight));
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) {
root.widgetX = Math.max(0, Math.min(root.screenWidth / 2 - root.widgetWidth / 2, root.screenWidth - root.widgetWidth));
root.widgetY = Math.max(0, Math.min(root.screenHeight / 2 - root.widgetHeight / 2, root.screenHeight - root.widgetHeight));
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);
@@ -355,6 +431,7 @@ Item {
id: dragArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
enabled: !root.clickThrough
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.ArrowCursor
property point startPos
@@ -367,6 +444,8 @@ Item {
startY = root.widgetY;
root.previewX = root.widgetX;
root.previewY = root.widgetY;
root.dragOverrideX = root.widgetX;
root.dragOverrideY = root.widgetY;
}
onPositionChanged: mouse => {
@@ -384,16 +463,15 @@ Item {
root.previewY = newY;
return;
}
root.widgetX = newX;
root.widgetY = newY;
root.dragOverrideX = newX;
root.dragOverrideY = newY;
}
onReleased: {
if (root.useGhostPreview) {
root.widgetX = root.previewX;
root.widgetY = root.previewY;
}
root.savePosition();
const finalX = root.useGhostPreview ? root.previewX : root.dragOverrideX;
const finalY = root.useGhostPreview ? root.previewY : root.dragOverrideY;
root.savePosition(finalX, finalY);
root.clearDragOverrides();
}
}
@@ -404,6 +482,7 @@ Item {
anchors.right: parent.right
anchors.bottom: parent.bottom
acceptedButtons: Qt.RightButton
enabled: !root.clickThrough
cursorShape: pressed ? Qt.SizeFDiagCursor : Qt.ArrowCursor
property point startPos
@@ -416,6 +495,8 @@ Item {
startHeight = root.widgetHeight;
root.previewWidth = root.widgetWidth;
root.previewHeight = root.widgetHeight;
root.dragOverrideW = root.widgetWidth;
root.dragOverrideH = root.widgetHeight;
}
onPositionChanged: mouse => {
@@ -438,16 +519,15 @@ Item {
root.previewHeight = newH;
return;
}
root.widgetWidth = newW;
root.widgetHeight = newH;
root.dragOverrideW = newW;
root.dragOverrideH = newH;
}
onReleased: {
if (root.useGhostPreview) {
root.widgetWidth = root.previewWidth;
root.widgetHeight = root.previewHeight;
}
root.saveSize();
const finalW = root.useGhostPreview ? root.previewWidth : root.dragOverrideW;
const finalH = root.useGhostPreview ? root.previewHeight : root.dragOverrideH;
root.saveSize(finalW, finalH);
root.clearDragOverrides();
}
}
}

View File

@@ -63,7 +63,13 @@ SettingsCard {
DankActionButton {
id: menuButton
iconName: "more_vert"
onClicked: actionsMenu.open()
onClicked: {
if (actionsMenu.opened) {
actionsMenu.close();
return;
}
actionsMenu.open();
}
Popup {
id: actionsMenu
@@ -71,7 +77,7 @@ SettingsCard {
y: parent.height + Theme.spacingXS
width: 160
padding: Theme.spacingXS
modal: true
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
@@ -218,6 +224,73 @@ SettingsCard {
SettingsDivider {}
Item {
width: parent.width
height: groupRow.height + Theme.spacingM * 2
visible: (SettingsData.desktopWidgetGroups || []).length > 0
Row {
id: groupRow
x: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
StyledText {
text: I18n.tr("Group")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: 80
horizontalAlignment: Text.AlignLeft
}
DankDropdown {
id: groupDropdown
width: parent.width - 80 - Theme.spacingM
compactMode: true
property var groupsData: {
const groups = SettingsData.desktopWidgetGroups || [];
const items = [
{
value: "",
label: I18n.tr("None")
}
];
for (const g of groups) {
items.push({
value: g.id,
label: g.name
});
}
return items;
}
options: groupsData.map(g => g.label)
currentValue: {
const currentGroup = root.instanceData?.group ?? "";
const item = groupsData.find(g => g.value === currentGroup);
return item?.label ?? I18n.tr("None");
}
onValueChanged: value => {
if (!root.instanceId)
return;
const item = groupsData.find(g => g.label === value);
const groupId = item?.value ?? "";
SettingsData.updateDesktopWidgetInstance(root.instanceId, {
group: groupId || null
});
}
}
}
}
SettingsDivider {
visible: (SettingsData.desktopWidgetGroups || []).length > 0
}
SettingsToggleRow {
text: I18n.tr("Show on Overlay")
checked: instanceData?.config?.showOnOverlay ?? false
@@ -266,6 +339,38 @@ SettingsCard {
SettingsDivider {}
SettingsToggleRow {
text: I18n.tr("Click Through")
description: I18n.tr("Allow clicks to pass through the widget")
checked: instanceData?.config?.clickThrough ?? false
onToggled: isChecked => {
if (!root.instanceId)
return;
SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, {
clickThrough: isChecked
});
}
}
SettingsDivider {}
SettingsToggleRow {
text: I18n.tr("Sync Position Across Screens")
description: I18n.tr("Use the same position and size on all displays")
checked: instanceData?.config?.syncPositionAcrossScreens ?? false
onToggled: isChecked => {
if (!root.instanceId)
return;
if (isChecked)
SettingsData.syncDesktopWidgetPositionToAllScreens(root.instanceId);
SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, {
syncPositionAcrossScreens: isChecked
});
}
}
SettingsDivider {}
Item {
width: parent.width
height: ipcColumn.height + Theme.spacingM * 2

View File

@@ -14,7 +14,13 @@ Item {
LayoutMirroring.childrenInherit: true
property var expandedStates: ({})
property var groupCollapsedStates: ({})
property var parentModal: null
property string editingGroupId: ""
property string newGroupName: ""
readonly property var allInstances: SettingsData.desktopWidgetInstances || []
readonly property var allGroups: SettingsData.desktopWidgetGroups || []
DesktopWidgetBrowser {
id: widgetBrowser
@@ -80,54 +86,500 @@ Item {
}
}
Column {
id: instancesColumn
SettingsCard {
width: parent.width
spacing: Theme.spacingM
visible: SettingsData.desktopWidgetInstances.length > 0
iconName: "folder"
title: I18n.tr("Groups")
collapsible: true
expanded: root.allGroups.length > 0
Repeater {
id: instanceRepeater
model: ScriptModel {
id: instancesModel
objectProp: "id"
values: SettingsData.desktopWidgetInstances
Column {
width: parent.width - Theme.spacingM * 2
x: Theme.spacingM
spacing: Theme.spacingM
StyledText {
width: parent.width
text: I18n.tr("Organize widgets into collapsible groups")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
}
DesktopWidgetInstanceCard {
required property var modelData
required property int index
Row {
spacing: Theme.spacingS
width: parent.width
readonly property string instanceIdRef: modelData.id
readonly property var liveInstanceData: {
const instances = SettingsData.desktopWidgetInstances || [];
return instances.find(inst => inst.id === instanceIdRef) ?? modelData;
DankTextField {
id: newGroupField
width: parent.width - addGroupBtn.width - Theme.spacingS
placeholderText: I18n.tr("New group name...")
text: root.newGroupName
onTextChanged: root.newGroupName = text
onAccepted: {
if (!text.trim())
return;
SettingsData.createDesktopWidgetGroup(text.trim());
root.newGroupName = "";
text = "";
}
}
width: instancesColumn.width
instanceData: liveInstanceData
isExpanded: root.expandedStates[instanceIdRef] ?? false
DankButton {
id: addGroupBtn
iconName: "add"
text: I18n.tr("Add")
enabled: root.newGroupName.trim().length > 0
onClicked: {
SettingsData.createDesktopWidgetGroup(root.newGroupName.trim());
root.newGroupName = "";
newGroupField.text = "";
}
}
}
onExpandedChanged: {
if (expanded === (root.expandedStates[instanceIdRef] ?? false))
return;
var states = Object.assign({}, root.expandedStates);
states[instanceIdRef] = expanded;
root.expandedStates = states;
Column {
width: parent.width
spacing: Theme.spacingXS
visible: root.allGroups.length > 0
Repeater {
model: root.allGroups
Rectangle {
id: groupItem
required property var modelData
required property int index
width: parent.width
height: 40
radius: Theme.cornerRadius
color: groupMouseArea.containsMouse ? Theme.surfaceHover : Theme.surfaceContainer
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: "folder"
size: Theme.iconSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Loader {
active: root.editingGroupId === groupItem.modelData.id
width: active ? parent.width - Theme.iconSizeSmall - deleteGroupBtn.width - Theme.spacingS * 3 : 0
height: active ? 32 : 0
anchors.verticalCenter: parent.verticalCenter
sourceComponent: DankTextField {
text: groupItem.modelData.name
onAccepted: {
if (!text.trim())
return;
SettingsData.updateDesktopWidgetGroup(groupItem.modelData.id, {
name: text.trim()
});
root.editingGroupId = "";
}
onEditingFinished: {
if (!text.trim())
return;
SettingsData.updateDesktopWidgetGroup(groupItem.modelData.id, {
name: text.trim()
});
root.editingGroupId = "";
}
Component.onCompleted: forceActiveFocus()
}
}
StyledText {
visible: root.editingGroupId !== groupItem.modelData.id
text: groupItem.modelData.name
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - Theme.iconSizeSmall - deleteGroupBtn.width - Theme.spacingS * 3
}
DankActionButton {
id: deleteGroupBtn
iconName: "delete"
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.removeDesktopWidgetGroup(groupItem.modelData.id);
ToastService.showInfo(I18n.tr("Group removed"));
}
}
}
MouseArea {
id: groupMouseArea
anchors.fill: parent
hoverEnabled: true
onDoubleClicked: root.editingGroupId = groupItem.modelData.id
}
}
}
}
}
}
Repeater {
model: root.allGroups
Column {
id: groupSection
required property var modelData
required property int index
readonly property string groupId: modelData.id
readonly property var groupInstances: root.allInstances.filter(inst => inst.group === groupId)
width: mainColumn.width
spacing: Theme.spacingM
visible: groupInstances.length > 0
Rectangle {
width: parent.width
height: 44
radius: Theme.cornerRadius
color: Theme.surfaceContainer
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: (root.groupCollapsedStates[groupSection.groupId] ?? false) ? "expand_more" : "expand_less"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
name: "folder"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: groupSection.modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "(" + groupSection.groupInstances.length + ")"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
onDuplicateRequested: SettingsData.duplicateDesktopWidgetInstance(instanceIdRef)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
var states = Object.assign({}, root.groupCollapsedStates);
states[groupSection.groupId] = !(states[groupSection.groupId] ?? false);
root.groupCollapsedStates = states;
}
}
}
onDeleteRequested: {
SettingsData.removeDesktopWidgetInstance(instanceIdRef);
ToastService.showInfo(I18n.tr("Widget removed"));
Column {
width: parent.width
spacing: Theme.spacingM
visible: !(root.groupCollapsedStates[groupSection.groupId] ?? false)
leftPadding: Theme.spacingM
Repeater {
model: ScriptModel {
objectProp: "id"
values: groupSection.groupInstances
}
Item {
id: groupDelegateItem
required property var modelData
required property int index
property bool held: groupDragArea.pressed
property real originalY: y
readonly property string instanceIdRef: modelData.id
readonly property var liveInstanceData: {
const instances = root.allInstances;
return instances.find(inst => inst.id === instanceIdRef) ?? modelData;
}
width: groupSection.width - Theme.spacingM
height: groupCard.height
z: held ? 2 : 1
DesktopWidgetInstanceCard {
id: groupCard
width: parent.width
headerLeftPadding: 20
instanceData: groupDelegateItem.liveInstanceData
isExpanded: root.expandedStates[groupDelegateItem.instanceIdRef] ?? false
onExpandedChanged: {
if (expanded === (root.expandedStates[groupDelegateItem.instanceIdRef] ?? false))
return;
var states = Object.assign({}, root.expandedStates);
states[groupDelegateItem.instanceIdRef] = expanded;
root.expandedStates = states;
}
onDuplicateRequested: SettingsData.duplicateDesktopWidgetInstance(groupDelegateItem.instanceIdRef)
onDeleteRequested: {
SettingsData.removeDesktopWidgetInstance(groupDelegateItem.instanceIdRef);
ToastService.showInfo(I18n.tr("Widget removed"));
}
}
MouseArea {
id: groupDragArea
anchors.left: parent.left
anchors.top: parent.top
width: 40
height: 50
hoverEnabled: true
cursorShape: Qt.SizeVerCursor
drag.target: groupDelegateItem.held ? groupDelegateItem : undefined
drag.axis: Drag.YAxis
preventStealing: true
onPressed: {
groupDelegateItem.z = 2;
groupDelegateItem.originalY = groupDelegateItem.y;
}
onReleased: {
groupDelegateItem.z = 1;
if (!drag.active) {
groupDelegateItem.y = groupDelegateItem.originalY;
return;
}
const spacing = Theme.spacingM;
const itemH = groupDelegateItem.height + spacing;
var newIndex = Math.round(groupDelegateItem.y / itemH);
newIndex = Math.max(0, Math.min(newIndex, groupSection.groupInstances.length - 1));
if (newIndex !== groupDelegateItem.index)
SettingsData.reorderDesktopWidgetInstanceInGroup(groupDelegateItem.instanceIdRef, groupSection.groupId, newIndex);
groupDelegateItem.y = groupDelegateItem.originalY;
}
}
DankIcon {
x: Theme.spacingL - 2
y: Theme.spacingL + (Theme.iconSize / 2) - (size / 2)
name: "drag_indicator"
size: 18
color: Theme.outline
opacity: groupDragArea.containsMouse || groupDragArea.pressed ? 1 : 0.5
}
Behavior on y {
enabled: !groupDragArea.pressed && !groupDragArea.drag.active
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
Column {
id: ungroupedSection
width: parent.width
spacing: Theme.spacingM
visible: ungroupedInstances.length > 0
readonly property var ungroupedInstances: root.allInstances.filter(inst => {
if (!inst.group)
return true;
return !root.allGroups.some(g => g.id === inst.group);
})
Rectangle {
width: parent.width
height: 44
radius: Theme.cornerRadius
color: Theme.surfaceContainer
visible: root.allGroups.length > 0
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: (root.groupCollapsedStates["_ungrouped"] ?? false) ? "expand_more" : "expand_less"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
name: "widgets"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Ungrouped")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "(" + ungroupedSection.ungroupedInstances.length + ")"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
var states = Object.assign({}, root.groupCollapsedStates);
states["_ungrouped"] = !(states["_ungrouped"] ?? false);
root.groupCollapsedStates = states;
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: !(root.groupCollapsedStates["_ungrouped"] ?? false)
leftPadding: root.allGroups.length > 0 ? Theme.spacingM : 0
Repeater {
model: ScriptModel {
objectProp: "id"
values: ungroupedSection.ungroupedInstances
}
Item {
id: ungroupedDelegateItem
required property var modelData
required property int index
property bool held: ungroupedDragArea.pressed
property real originalY: y
readonly property string instanceIdRef: modelData.id
readonly property var liveInstanceData: {
const instances = root.allInstances;
return instances.find(inst => inst.id === instanceIdRef) ?? modelData;
}
width: ungroupedSection.width - (root.allGroups.length > 0 ? Theme.spacingM : 0)
height: ungroupedCard.height
z: held ? 2 : 1
DesktopWidgetInstanceCard {
id: ungroupedCard
width: parent.width
headerLeftPadding: 20
instanceData: ungroupedDelegateItem.liveInstanceData
isExpanded: root.expandedStates[ungroupedDelegateItem.instanceIdRef] ?? false
onExpandedChanged: {
if (expanded === (root.expandedStates[ungroupedDelegateItem.instanceIdRef] ?? false))
return;
var states = Object.assign({}, root.expandedStates);
states[ungroupedDelegateItem.instanceIdRef] = expanded;
root.expandedStates = states;
}
onDuplicateRequested: SettingsData.duplicateDesktopWidgetInstance(ungroupedDelegateItem.instanceIdRef)
onDeleteRequested: {
SettingsData.removeDesktopWidgetInstance(ungroupedDelegateItem.instanceIdRef);
ToastService.showInfo(I18n.tr("Widget removed"));
}
}
MouseArea {
id: ungroupedDragArea
anchors.left: parent.left
anchors.top: parent.top
width: 40
height: 50
hoverEnabled: true
cursorShape: Qt.SizeVerCursor
drag.target: ungroupedDelegateItem.held ? ungroupedDelegateItem : undefined
drag.axis: Drag.YAxis
preventStealing: true
onPressed: {
ungroupedDelegateItem.z = 2;
ungroupedDelegateItem.originalY = ungroupedDelegateItem.y;
}
onReleased: {
ungroupedDelegateItem.z = 1;
if (!drag.active) {
ungroupedDelegateItem.y = ungroupedDelegateItem.originalY;
return;
}
const spacing = Theme.spacingM;
const itemH = ungroupedDelegateItem.height + spacing;
var newIndex = Math.round(ungroupedDelegateItem.y / itemH);
newIndex = Math.max(0, Math.min(newIndex, ungroupedSection.ungroupedInstances.length - 1));
if (newIndex !== ungroupedDelegateItem.index)
SettingsData.reorderDesktopWidgetInstanceInGroup(ungroupedDelegateItem.instanceIdRef, null, newIndex);
ungroupedDelegateItem.y = ungroupedDelegateItem.originalY;
}
}
DankIcon {
x: Theme.spacingL - 2
y: Theme.spacingL + (Theme.iconSize / 2) - (size / 2)
name: "drag_indicator"
size: 18
color: Theme.outline
opacity: ungroupedDragArea.containsMouse || ungroupedDragArea.pressed ? 1 : 0.5
}
Behavior on y {
enabled: !ungroupedDragArea.pressed && !ungroupedDragArea.drag.active
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
StyledText {
visible: SettingsData.desktopWidgetInstances.length === 0
visible: root.allInstances.length === 0
text: I18n.tr("No widgets added. Click \"Add Widget\" to get started.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText

View File

@@ -19,6 +19,7 @@ StyledRect {
property string iconName: ""
property bool collapsible: false
property bool expanded: true
property real headerLeftPadding: 0
default property alias content: contentColumn.children
property alias headerActions: headerActionsRow.children
@@ -115,6 +116,7 @@ StyledRect {
Row {
anchors.left: parent.left
anchors.leftMargin: root.headerLeftPadding
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM