Files
bbedward 4b0dfa67a0 desktop: add right click context menu for actions on compositors where
it doesnt interfere, with a setting to turn it off

Some compositors like kwin/labwc/sway cannot have this without breaking
some built in layering they do.

niri can have this, but for focus follow mouse it may need to be disabled.

fixes #3717
2026-10-07 15:18:39 -04:00

308 lines
10 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.DCommon.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var groupCollapsedStates: ({})
property var parentModal: null
property string newGroupName: ""
property string renamingGroupId: ""
readonly property var allInstances: SettingsData.desktopWidgetInstances || []
readonly property var allGroups: SettingsData.desktopWidgetGroups || []
readonly property bool dragActive: dragGroup.active
property alias reorderGroup: dragGroup
SettingsReorderGroup {
id: dragGroup
coordinateItem: root
onTransferred: (source, sourceIndex, target, targetIndex) => SettingsData.moveDesktopWidgetInstanceToGroup(source.model[sourceIndex].id, target.groupKey || null, targetIndex)
}
function storageKeyFor(sectionKey) {
return sectionKey === "" ? "_ungrouped" : sectionKey;
}
function toggleCollapsed(sectionKey) {
const key = storageKeyFor(sectionKey);
var states = Object.assign({}, groupCollapsedStates);
states[key] = !(states[key] ?? false);
groupCollapsedStates = states;
}
function configureWidget(instanceId, title) {
SettingsUiState.selectedDesktopWidgetId = instanceId;
SettingsUiState.selectedWidgetTitle = title;
parentModal?.navigateTo("desktop_widget");
}
function openRenameDialog(group) {
renamingGroupId = group.id;
renameDialog.show(group.name);
}
function saveGroupName(name) {
SettingsData.updateDesktopWidgetGroup(renamingGroupId, {
name: name
});
renameDialog.hide();
}
function showWidgetBrowser() {
widgetBrowserLoader.active = true;
if (widgetBrowserLoader.item)
widgetBrowserLoader.item.show();
}
function showDesktopPluginBrowser() {
desktopPluginBrowserLoader.active = true;
if (desktopPluginBrowserLoader.item)
desktopPluginBrowserLoader.item.show();
}
LazyLoader {
id: widgetBrowserLoader
active: false
DesktopWidgetBrowser {
parentModal: root.parentModal
onWidgetAdded: widgetType => {
ToastService.showInfo(I18n.tr("Widget added"));
}
}
}
LazyLoader {
id: desktopPluginBrowserLoader
active: false
PluginBrowser {
parentModal: root.parentModal
typeFilter: "desktop-widget"
}
}
SettingsPage {
id: mainColumn
SettingsCard {
settingKey: "desktopGeneral"
tags: ["desktop", "context", "menu", "right click", "wallpaper"]
width: parent.width
iconName: "mouse"
title: I18n.tr("General")
SettingsDropdownRow {
settingKey: "desktopContextMenu"
text: I18n.tr("Right-click menu")
description: I18n.tr("Auto turns it off on compositors that use clicks on the empty desktop themselves", "desktop context menu dropdown description")
options: [I18n.tr("Auto"), I18n.tr("On"), I18n.tr("Off")]
readonly property var values: ["auto", "on", "off"]
currentValue: options[Math.max(0, values.indexOf(SettingsData.desktopContextMenu))]
onValueChanged: value => SettingsData.set("desktopContextMenu", values[options.indexOf(value)])
}
}
SettingsCard {
settingKey: "desktopWidgetGroups"
tags: ["groups", "profiles", "layouts"]
width: parent.width
iconName: "folder"
title: I18n.tr("Groups", "noun, card title for desktop widget groups")
collapsible: true
expanded: root.allGroups.length > 0
SettingsRow {
body: Row {
spacing: Theme.spacingS
width: parent.width
DTextField {
id: newGroupField
outlined: true
leftIconName: "folder"
labelText: I18n.tr("Name")
width: parent.width - addGroupBtn.width - Theme.spacingS
text: root.newGroupName
onTextChanged: root.newGroupName = text
onAccepted: {
if (!text.trim())
return;
SettingsData.createDesktopWidgetGroup(text.trim());
root.newGroupName = "";
text = "";
}
}
DButton {
id: addGroupBtn
iconName: "add"
text: I18n.tr("Add")
enabled: root.newGroupName.trim().length > 0
onClicked: {
SettingsData.createDesktopWidgetGroup(root.newGroupName.trim());
root.newGroupName = "";
newGroupField.text = "";
}
}
}
}
Repeater {
model: root.allGroups
delegate: SettingsRow {
id: groupRow
required property var modelData
iconName: "folder"
title: modelData.name
singleLineTitle: true
DActionButton {
iconName: "edit"
tooltipText: I18n.tr("Rename")
onClicked: root.openRenameDialog(groupRow.modelData)
}
DActionButton {
iconName: "delete"
iconColor: Theme.error
tooltipText: I18n.tr("Delete")
onClicked: {
SettingsData.removeDesktopWidgetGroup(groupRow.modelData.id);
ToastService.showInfo(I18n.tr("Group removed"));
}
}
}
}
}
Repeater {
id: groupsRepeater
model: root.allGroups
DesktopWidgetGroupSection {
required property var modelData
required property int index
width: mainColumn.columnWidth
reorderGroup: dragGroup
groupId: modelData.id
groupName: modelData.name
isUngrouped: false
showHeader: true
collapsed: root.groupCollapsedStates[modelData.id] ?? false
instances: root.allInstances.filter(inst => inst.group === modelData.id)
visible: instances.length > 0 || root.dragActive
onCollapseToggled: key => root.toggleCollapsed(key)
onConfigureRequested: (instanceId, title) => root.configureWidget(instanceId, title)
onDuplicateRequested: instanceId => SettingsData.duplicateDesktopWidgetInstance(instanceId)
onDeleteRequested: instanceId => {
SettingsData.removeDesktopWidgetInstance(instanceId);
ToastService.showInfo(I18n.tr("Widget removed"));
}
}
}
DesktopWidgetGroupSection {
id: ungroupedSection
readonly property var ungroupedInstances: root.allInstances.filter(inst => {
if (!inst.group)
return true;
return !root.allGroups.some(g => g.id === inst.group);
})
width: mainColumn.columnWidth
reorderGroup: dragGroup
groupId: null
groupName: I18n.tr("Ungrouped", "section header for desktop widgets without a group")
isUngrouped: true
showHeader: root.allGroups.length > 0
collapsed: root.groupCollapsedStates["_ungrouped"] ?? false
instances: ungroupedInstances
visible: ungroupedInstances.length > 0 || root.dragActive
onCollapseToggled: key => root.toggleCollapsed(key)
onConfigureRequested: (instanceId, title) => root.configureWidget(instanceId, title)
onDuplicateRequested: instanceId => SettingsData.duplicateDesktopWidgetInstance(instanceId)
onDeleteRequested: instanceId => {
SettingsData.removeDesktopWidgetInstance(instanceId);
ToastService.showInfo(I18n.tr("Widget removed"));
}
}
StyledText {
visible: root.allInstances.length === 0
text: I18n.tr("No widgets added. Click \"Add widget\" to get started.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
}
SettingsCard {
width: parent.width
iconName: "info"
title: I18n.tr("Help", "noun, card title for desktop widget usage tips")
SettingsRow {
iconName: "drag_indicator"
iconBox: true
title: I18n.tr("Reorder & group")
subtitle: I18n.tr("Drag a widget by its handle here to reorder it or drop it into another group")
}
}
SettingsFabBar {
DFab {
text: I18n.tr("Browse plugins")
iconName: "store"
colorRole: "secondaryContainer"
onClicked: root.showDesktopPluginBrowser()
}
DFab {
text: I18n.tr("Edit widgets")
iconName: "edit"
colorRole: "secondaryContainer"
onClicked: DesktopWidgetRegistry.editing = true
}
DFab {
text: I18n.tr("Add widget")
iconName: "add"
onClicked: root.showWidgetBrowser()
}
}
}
SettingsReorderPreview {
group: dragGroup
}
SettingsRenameDialog {
id: renameDialog
parent: root.parentModal?.modalFocusScope ?? root
supportingText: root.allGroups.find(g => g.id === root.renamingGroupId)?.name ?? ""
leftIconName: "folder"
onAccepted: name => root.saveGroupName(name)
}
}