From 00952d4d51ebfb3dde4eec6b8b8e1517aaec5031 Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 29 Sep 2026 09:17:17 -0400 Subject: [PATCH] settings: window rule editor on the shortcut editor's dialog and cards fixes #3608 --- quickshell/Modals/WindowRuleModal.qml | 1653 +---------------- .../Settings/WindowRuleEditorDialog.qml | 1203 ++++++++++++ .../Modules/Settings/WindowRulesTab.qml | 92 +- 3 files changed, 1283 insertions(+), 1665 deletions(-) create mode 100644 quickshell/Modules/Settings/WindowRuleEditorDialog.qml diff --git a/quickshell/Modals/WindowRuleModal.qml b/quickshell/Modals/WindowRuleModal.qml index bcf803f0e..73829f86c 100644 --- a/quickshell/Modals/WindowRuleModal.qml +++ b/quickshell/Modals/WindowRuleModal.qml @@ -1,1675 +1,52 @@ import QtQuick +import QtQuick.Window import qs.Common -import qs.Services +import qs.Modules.Settings +import qs.Modules.Settings.Widgets import qs.Widgets DankFloatingWindow { id: root - property var editingRule: null - property bool isEditMode: editingRule !== null - property bool isNiri: CompositorService.isNiri - property bool isHyprland: CompositorService.isHyprland - property bool isMango: CompositorService.isMango - property bool submitting: false - property var targetWindow: null - signal ruleSubmitted - readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 - readonly property int sectionSpacing: Theme.spacingL - - ListModel { - id: extraMatchModel - } - objectName: "windowRuleModal" - title: isEditMode ? I18n.tr("Edit Window Rule") : I18n.tr("Create Window Rule") - minimumSize: Qt.size(500, 600) - maximumSize: Qt.size(500, 600) + title: editor.title + minimumSize: Qt.size(Math.min(SettingsMetrics.formDialogWidth + Theme.spacingL * 2, Screen.width), Math.min(Math.round(Theme.fontSizeMedium * 52), Screen.height)) + maximumSize: minimumSize visible: false onClosed: hide() - function resetForm() { - nameInput.text = ""; - appIdInput.text = ""; - titleInput.text = ""; - extraMatchModel.clear(); - condFloating.triState = 0; - condActive.triState = 0; - condFocused.triState = 0; - condActiveInColumn.triState = 0; - condCastTarget.triState = 0; - condUrgent.triState = 0; - condAtStartup.triState = 0; - condXwayland.triState = 0; - condFullscreen.triState = 0; - condPinned.triState = 0; - condInitialised.triState = 0; - opacityEnabled.checked = false; - opacitySlider.value = 100; - floatingCond.triState = 0; - floatingToggle.checked = false; - maximizedToggle.checked = false; - maximizedToEdgesToggle.checked = false; - fullscreenToggle.checked = false; - openFocusedToggle.checked = false; - outputInput.text = ""; - workspaceInput.text = ""; - columnWidthInput.text = ""; - windowHeightInput.text = ""; - vrrToggle.checked = false; - blockOutDropdown.currentValue = ""; - columnDisplayDropdown.currentValue = ""; - scrollFactorEnabled.checked = false; - scrollFactorSlider.value = 100; - cornerRadiusEnabled.checked = false; - cornerRadiusSlider.value = 12; - clipToGeometryToggle.checked = false; - tiledStateToggle.checked = false; - drawBorderBgToggle.checked = false; - blurCond.triState = 0; - xrayCond.triState = 0; - noiseEnabled.checked = false; - noiseSlider.value = 5; - saturationEnabled.checked = false; - saturationSlider.value = 100; - floatingXInput.text = ""; - floatingYInput.text = ""; - floatingRelativeDropdown.currentValue = "top-left"; - minWidthInput.text = ""; - maxWidthInput.text = ""; - minHeightInput.text = ""; - maxHeightInput.text = ""; - tileToggle.checked = false; - noFocusToggle.checked = false; - noBorderToggle.checked = false; - noShadowToggle.checked = false; - noDimToggle.checked = false; - noBlurToggle.checked = false; - noAnimToggle.checked = false; - noRoundingToggle.checked = false; - pinToggle.checked = false; - opaqueToggle.checked = false; - moveXInput.text = ""; - moveYInput.text = ""; - sizeWInput.text = ""; - sizeHInput.text = ""; - monitorInput.text = ""; - hyprWorkspaceInput.text = ""; - mangoTagsInput.text = ""; - mangoMonitorInput.text = ""; - mangoSizeInput.text = ""; - mangoNoBlurToggle.checked = false; - mangoNoBorderToggle.checked = false; - mangoNoShadowToggle.checked = false; - mangoNoRoundingToggle.checked = false; - mangoNoAnimToggle.checked = false; - } - function show(window) { - editingRule = null; - targetWindow = window || null; - resetForm(); - if (targetWindow) { - nameInput.text = targetWindow.appId || ""; - if (targetWindow.appId) - appIdInput.text = isMango ? targetWindow.appId : "^" + targetWindow.appId + "$"; - else - appIdInput.text = ""; - } visible = true; - Qt.callLater(() => nameInput.forceActiveFocus()); - } - - function triFromBool(v) { - if (v === true) - return 1; - if (v === false) - return 2; - return 0; - } - - function populateForm(rule) { - nameInput.text = rule.name || ""; - const matchList = (rule.matches && rule.matches.length > 0) ? rule.matches : [rule.matchCriteria || {}]; - const match = matchList[0] || {}; - appIdInput.text = match.appId || ""; - titleInput.text = match.title || ""; - extraMatchModel.clear(); - for (let i = 1; i < matchList.length; i++) { - extraMatchModel.append({ - "rowAppId": matchList[i].appId || "", - "rowTitle": matchList[i].title || "" - }); - } - - condFloating.triState = triFromBool(match.isFloating); - condActive.triState = triFromBool(match.isActive); - condFocused.triState = triFromBool(match.isFocused); - condActiveInColumn.triState = triFromBool(match.isActiveInColumn); - condCastTarget.triState = triFromBool(match.isWindowCastTarget); - condUrgent.triState = triFromBool(match.isUrgent); - condAtStartup.triState = triFromBool(match.atStartup); - condXwayland.triState = triFromBool(match.xwayland); - condFullscreen.triState = triFromBool(match.fullscreen); - condPinned.triState = triFromBool(match.pinned); - condInitialised.triState = triFromBool(match.initialised); - - const actions = rule.actions || {}; - const hasOpacity = actions.opacity !== undefined && actions.opacity !== null; - opacityEnabled.checked = hasOpacity; - opacitySlider.value = hasOpacity ? Math.round(actions.opacity * 100) : 100; - - floatingCond.triState = triFromBool(actions.openFloating); - floatingToggle.checked = actions.openFloating || false; - maximizedToggle.checked = actions.openMaximized || false; - maximizedToEdgesToggle.checked = actions.openMaximizedToEdges || false; - fullscreenToggle.checked = actions.openFullscreen || false; - - openFocusedToggle.checked = actions.openFocused || false; - - outputInput.text = actions.openOnOutput || ""; - workspaceInput.text = actions.openOnWorkspace || ""; - columnWidthInput.text = actions.defaultColumnWidth || ""; - windowHeightInput.text = actions.defaultWindowHeight || ""; - vrrToggle.checked = actions.variableRefreshRate || false; - - blockOutDropdown.currentValue = actions.blockOutFrom || ""; - columnDisplayDropdown.currentValue = actions.defaultColumnDisplay || ""; - - const hasScrollFactor = actions.scrollFactor !== undefined && actions.scrollFactor !== null; - scrollFactorEnabled.checked = hasScrollFactor; - scrollFactorSlider.value = hasScrollFactor ? Math.round(actions.scrollFactor * 100) : 100; - - const hasCornerRadius = actions.cornerRadius !== undefined && actions.cornerRadius !== null; - cornerRadiusEnabled.checked = hasCornerRadius; - cornerRadiusSlider.value = hasCornerRadius ? actions.cornerRadius : 12; - - clipToGeometryToggle.checked = actions.clipToGeometry || false; - tiledStateToggle.checked = actions.tiledState || false; - - drawBorderBgToggle.checked = actions.drawBorderWithBackground || false; - - xrayCond.triState = triFromBool(actions.backgroundXray); - blurCond.triState = triFromBool(actions.backgroundBlur); - const hasNoise = actions.backgroundNoise !== undefined && actions.backgroundNoise !== null; - noiseEnabled.checked = hasNoise; - noiseSlider.value = hasNoise ? Math.round(actions.backgroundNoise * 100) : 5; - const hasSaturation = actions.backgroundSaturation !== undefined && actions.backgroundSaturation !== null; - saturationEnabled.checked = hasSaturation; - saturationSlider.value = hasSaturation ? Math.round(actions.backgroundSaturation * 100) : 100; - - floatingXInput.text = (actions.defaultFloatingX !== undefined && actions.defaultFloatingX !== null) ? String(actions.defaultFloatingX) : ""; - floatingYInput.text = (actions.defaultFloatingY !== undefined && actions.defaultFloatingY !== null) ? String(actions.defaultFloatingY) : ""; - floatingRelativeDropdown.currentValue = actions.defaultFloatingRelativeTo || "top-left"; - - minWidthInput.text = actions.minWidth !== undefined ? String(actions.minWidth) : ""; - maxWidthInput.text = actions.maxWidth !== undefined ? String(actions.maxWidth) : ""; - minHeightInput.text = actions.minHeight !== undefined ? String(actions.minHeight) : ""; - maxHeightInput.text = actions.maxHeight !== undefined ? String(actions.maxHeight) : ""; - - tileToggle.checked = actions.tile || false; - noFocusToggle.checked = actions.nofocus || false; - noBorderToggle.checked = actions.noborder || false; - noShadowToggle.checked = actions.noshadow || false; - noDimToggle.checked = actions.nodim || false; - noBlurToggle.checked = actions.noblur || false; - noAnimToggle.checked = actions.noanim || false; - noRoundingToggle.checked = actions.norounding || false; - pinToggle.checked = actions.pin || false; - opaqueToggle.checked = actions.opaque || false; - moveXInput.text = actions.moveX || ""; - moveYInput.text = actions.moveY || ""; - sizeWInput.text = actions.sizeWidth || ""; - sizeHInput.text = actions.sizeHeight || ""; - monitorInput.text = actions.monitor || ""; - hyprWorkspaceInput.text = actions.workspace || ""; - - mangoTagsInput.text = actions.workspace || ""; - mangoMonitorInput.text = actions.monitor || ""; - mangoSizeInput.text = (actions.sizeWidth && actions.sizeHeight) ? actions.sizeWidth + "x" + actions.sizeHeight : ""; - mangoNoBlurToggle.checked = actions.noblur || false; - mangoNoBorderToggle.checked = actions.noborder || false; - mangoNoShadowToggle.checked = actions.noshadow || false; - mangoNoRoundingToggle.checked = actions.norounding || false; - mangoNoAnimToggle.checked = actions.noanim || false; + editor.show(window); } function showEdit(rule) { - if (!rule) { - show(); - return; - } - editingRule = rule; - resetForm(); - populateForm(rule); visible = true; - Qt.callLater(() => nameInput.forceActiveFocus()); + editor.showEdit(rule); } function showCopy(rule) { - if (!rule) { - show(); - return; - } - editingRule = null; - resetForm(); - populateForm(rule); visible = true; - Qt.callLater(() => nameInput.forceActiveFocus()); + editor.showCopy(rule); } function hide() { + editor.opened = false; visible = false; - editingRule = null; - targetWindow = null; } - function applyCond(obj, key, triState) { - if (triState === 1) - obj[key] = true; - else if (triState === 2) - obj[key] = false; - } - - function submitAndClose() { - if (submitting) - return; - const matchCriteria = {}; - if (appIdInput.text.trim()) - matchCriteria.appId = appIdInput.text.trim(); - if (titleInput.text.trim()) - matchCriteria.title = titleInput.text.trim(); - - applyCond(matchCriteria, "isFloating", condFloating.triState); - if (isNiri) { - applyCond(matchCriteria, "isActive", condActive.triState); - applyCond(matchCriteria, "isFocused", condFocused.triState); - applyCond(matchCriteria, "isActiveInColumn", condActiveInColumn.triState); - applyCond(matchCriteria, "isWindowCastTarget", condCastTarget.triState); - applyCond(matchCriteria, "isUrgent", condUrgent.triState); - applyCond(matchCriteria, "atStartup", condAtStartup.triState); - } - if (isHyprland) { - applyCond(matchCriteria, "xwayland", condXwayland.triState); - applyCond(matchCriteria, "fullscreen", condFullscreen.triState); - applyCond(matchCriteria, "pinned", condPinned.triState); - applyCond(matchCriteria, "initialised", condInitialised.triState); - } - - const matches = []; - if (Object.keys(matchCriteria).length > 0) - matches.push(matchCriteria); - if (isNiri) { - for (let i = 0; i < extraMatchModel.count; i++) { - const row = extraMatchModel.get(i); - const m = {}; - if ((row.rowAppId || "").trim()) - m.appId = row.rowAppId.trim(); - if ((row.rowTitle || "").trim()) - m.title = row.rowTitle.trim(); - if (Object.keys(m).length > 0) - matches.push(m); - } - } - - const actions = {}; - - if (opacityEnabled.checked) - actions.opacity = opacitySlider.value / 100; - if (isNiri) - applyCond(actions, "openFloating", floatingCond.triState); - else if (floatingToggle.checked) - actions.openFloating = true; - if (maximizedToggle.checked) - actions.openMaximized = true; - if (maximizedToEdgesToggle.checked && isNiri) - actions.openMaximizedToEdges = true; - if (fullscreenToggle.checked) - actions.openFullscreen = true; - if (openFocusedToggle.checked && isNiri) - actions.openFocused = true; - if (outputInput.text.trim()) - actions.openOnOutput = outputInput.text.trim(); - if (workspaceInput.text.trim()) - actions.openOnWorkspace = workspaceInput.text.trim(); - if (columnWidthInput.text.trim() && isNiri) - actions.defaultColumnWidth = columnWidthInput.text.trim(); - if (windowHeightInput.text.trim() && isNiri) - actions.defaultWindowHeight = windowHeightInput.text.trim(); - if (vrrToggle.checked && isNiri) - actions.variableRefreshRate = true; - if (blockOutDropdown.currentValue && isNiri) - actions.blockOutFrom = blockOutDropdown.currentValue; - if (columnDisplayDropdown.currentValue && isNiri) - actions.defaultColumnDisplay = columnDisplayDropdown.currentValue; - if (scrollFactorEnabled.checked && isNiri) - actions.scrollFactor = scrollFactorSlider.value / 100; - if (cornerRadiusEnabled.checked) - actions.cornerRadius = cornerRadiusSlider.value; - if (clipToGeometryToggle.checked && isNiri) - actions.clipToGeometry = true; - if (tiledStateToggle.checked && isNiri) - actions.tiledState = true; - if (drawBorderBgToggle.checked && isNiri) - actions.drawBorderWithBackground = true; - if (isNiri) { - applyCond(actions, "backgroundBlur", blurCond.triState); - applyCond(actions, "backgroundXray", xrayCond.triState); - } - if (noiseEnabled.checked && isNiri) - actions.backgroundNoise = noiseSlider.value / 100; - if (saturationEnabled.checked && isNiri) - actions.backgroundSaturation = saturationSlider.value / 100; - - const floatX = parseInt(floatingXInput.text); - const floatY = parseInt(floatingYInput.text); - if (isNiri && !isNaN(floatX) && !isNaN(floatY)) { - actions.defaultFloatingX = floatX; - actions.defaultFloatingY = floatY; - if (floatingRelativeDropdown.currentValue && floatingRelativeDropdown.currentValue !== "top-left") - actions.defaultFloatingRelativeTo = floatingRelativeDropdown.currentValue; - } - - const minW = parseInt(minWidthInput.text); - const maxW = parseInt(maxWidthInput.text); - const minH = parseInt(minHeightInput.text); - const maxH = parseInt(maxHeightInput.text); - if (!isNaN(minW)) - actions.minWidth = minW; - if (!isNaN(maxW)) - actions.maxWidth = maxW; - if (!isNaN(minH)) - actions.minHeight = minH; - if (!isNaN(maxH)) - actions.maxHeight = maxH; - - if (isHyprland) { - if (tileToggle.checked) - actions.tile = true; - if (noFocusToggle.checked) - actions.nofocus = true; - if (noBorderToggle.checked) - actions.noborder = true; - if (noShadowToggle.checked) - actions.noshadow = true; - if (noDimToggle.checked) - actions.nodim = true; - if (noBlurToggle.checked) - actions.noblur = true; - if (noAnimToggle.checked) - actions.noanim = true; - if (noRoundingToggle.checked) - actions.norounding = true; - if (pinToggle.checked) - actions.pin = true; - if (opaqueToggle.checked) - actions.opaque = true; - if (sizeWInput.text.trim()) - actions.sizeWidth = sizeWInput.text.trim(); - if (sizeHInput.text.trim()) - actions.sizeHeight = sizeHInput.text.trim(); - if (moveXInput.text.trim()) - actions.moveX = moveXInput.text.trim(); - if (moveYInput.text.trim()) - actions.moveY = moveYInput.text.trim(); - if (monitorInput.text.trim()) - actions.monitor = monitorInput.text.trim(); - if (hyprWorkspaceInput.text.trim()) - actions.workspace = hyprWorkspaceInput.text.trim(); - } - - if (isMango) { - if (mangoTagsInput.text.trim()) - actions.workspace = mangoTagsInput.text.trim(); - if (mangoMonitorInput.text.trim()) - actions.monitor = mangoMonitorInput.text.trim(); - if (mangoSizeInput.text.trim()) { - const parts = mangoSizeInput.text.trim().split(/x/i); - if (parts.length === 2) { - actions.sizeWidth = parts[0].trim(); - actions.sizeHeight = parts[1].trim(); - } - } - if (mangoNoBlurToggle.checked) - actions.noblur = true; - if (mangoNoBorderToggle.checked) - actions.noborder = true; - if (mangoNoShadowToggle.checked) - actions.noshadow = true; - if (mangoNoRoundingToggle.checked) - actions.norounding = true; - if (mangoNoAnimToggle.checked) - actions.noanim = true; - } - - const name = nameInput.text.trim() || matchCriteria.appId || I18n.tr("Rule", "noun, fallback name for an unnamed window rule"); - const compositor = CompositorService.compositor; - - const ruleData = { - name: name, - matchCriteria: matchCriteria, - actions: actions, - enabled: true - }; - if (isNiri && extraMatchModel.count > 0) - ruleData.matches = matches; - // No exclude editor yet — carry existing excludes through so an edit doesn't delete them (#2996) - if (isEditMode && (editingRule.excludes?.length ?? 0) > 0) - ruleData.excludes = editingRule.excludes; - - submitting = true; - - const shouldValidate = CompositorService.isNiri; - - if (isEditMode) { - const ruleJson = JSON.stringify(ruleData); - Proc.runCommand("update-windowrule", [Proc.dmsBin, "config", "windowrules", "update", compositor, editingRule.id, ruleJson], (output, exitCode) => { - root.submitting = false; - if (exitCode !== 0) - return; - if (shouldValidate) - NiriService.validate(); - if (CompositorService.isMango) - MangoService.reloadConfig(); - root.ruleSubmitted(); - root.hide(); - }); - } else { - const ruleJson = JSON.stringify(ruleData); - Proc.runCommand("add-windowrule", [Proc.dmsBin, "config", "windowrules", "add", compositor, ruleJson], (output, exitCode) => { - root.submitting = false; - if (exitCode !== 0) - return; - if (shouldValidate) - NiriService.validate(); - if (CompositorService.isMango) - MangoService.reloadConfig(); - root.ruleSubmitted(); - root.hide(); - }); - } - } - - onVisibleChanged: { - if (!visible) { - editingRule = null; - targetWindow = null; - } - } - - component SectionHeader: StyledText { - property string title - text: title - font.pixelSize: Theme.fontSizeMedium - font.weight: Theme.fontWeightMedium - color: Theme.primary - topPadding: Theme.spacingM - bottomPadding: Theme.spacingXS - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - component CheckboxRow: DankButton { - property string label: "" - property bool indeterminate: false - - text: label - maximumWidth: parent.width - wrapText: true - iconName: indeterminate ? "indeterminate_check_box" : checked ? "check_box" : "check_box_outline_blank" - backgroundColor: checked ? Theme.secondaryContainer : "transparent" - textColor: checked ? Theme.onSecondaryContainer : Theme.onSurface - shape: checked ? "round" : "square" - checkable: true - Accessible.role: Accessible.CheckBox - Accessible.checked: checked - onClicked: { - if (indeterminate) { - indeterminate = false; - checked = true; - return; - } - checked = !checked; - } - } - - component TriCheckboxRow: DankButton { - property string label: "" - property int triState: 0 - readonly property bool forcedOff: triState === 2 - - text: forcedOff ? label + ": " + I18n.tr("Off") : label - maximumWidth: parent.width - wrapText: true - iconName: triState === 1 ? "check_box" : forcedOff ? "disabled_by_default" : "check_box_outline_blank" - backgroundColor: triState === 0 ? "transparent" : forcedOff ? Theme.errorContainer : Theme.secondaryContainer - textColor: triState === 0 ? Theme.onSurface : forcedOff ? Theme.onErrorContainer : Theme.onSecondaryContainer - shape: triState === 0 ? "square" : "round" - checkable: true - checked: triState !== 0 - Accessible.role: Accessible.CheckBox - Accessible.checked: triState === 1 - onClicked: triState = (triState + 1) % 3 - } - - component MatchCond: StyledButton { - id: mc - property string label: "" - property int triState: 0 - property string unsetLabel: I18n.tr("Default") - property bool readOnly: false - readonly property var stateText: [mc.unsetLabel, "true", "false"] - readonly property var stateColor: [Theme.surfaceVariantText, Theme.primary, Theme.error] - - width: condRow.implicitWidth + Theme.spacingM * 2 - height: root.inputFieldHeight - radius: Theme.cornerRadiusM - color: enabled ? Theme.chipSurface : Theme.onSurface_12 - border.width: Theme.outlineWidth - border.color: !enabled ? "transparent" : mc.triState === 0 ? Theme.outlineStrong : mc.stateColor[mc.triState] - enabled: root.visible && !mc.readOnly - Accessible.name: label + ": " + stateText[triState] - onClicked: triState = (triState + 1) % 3 - - Row { - id: condRow - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: mc.label - font.pixelSize: Theme.fontSizeSmall - color: mc.enabled ? Theme.onSurface : Theme.onSurface_38 - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - width: stateBadge.implicitWidth + Theme.spacingS * 2 - height: 18 - radius: Theme.fullRadius(width, height) - color: mc.enabled ? Theme.withAlpha(mc.stateColor[mc.triState], Theme.stateLayerPressed) : "transparent" - anchors.verticalCenter: parent.verticalCenter - - StyledText { - id: stateBadge - anchors.centerIn: parent - text: mc.stateText[mc.triState] - font.pixelSize: Theme.fontSizeSmall - 2 - font.weight: Theme.fontWeightMedium - color: mc.enabled ? mc.stateColor[mc.triState] : Theme.onSurface_38 - } - } - } - - StateLayer { - control: mc - disabled: !mc.enabled - stateColor: Theme.primary - } - - FocusRing { - visible: mc.visualFocus - } - } - - DankDialog { - id: ruleDialog - + WindowRuleEditorDialog { + id: editor anchors.fill: parent windowControls: ruleWindowControls - title: root.isEditMode ? I18n.tr("Edit Window Rule") : I18n.tr("New Window Rule") - supportingText: I18n.tr("Configure match criteria and actions") - acceptEnabled: !root.submitting onRejected: root.hide() - onAccepted: root.submitAndClose() - - Column { - id: contentCol - width: parent.width - spacing: Theme.spacingXS - - DankTextField { - id: nameInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Rule Name") - leftIconName: "edit" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - enabled: root.visible - } - - SectionHeader { - title: I18n.tr("Match Criteria") - } - - DankTextField { - id: appIdInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: isMango ? I18n.tr("App ID (e.g. firefox)") : isHyprland ? I18n.tr("Class regex (e.g. ^firefox$)") : I18n.tr("App ID regex (e.g. ^firefox$)") - leftIconName: "apps" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - enabled: root.visible - } - - Row { - width: parent.width - spacing: Theme.spacingS - - DankTextField { - id: titleInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: isMango ? I18n.tr("Title (optional)") : I18n.tr("Title regex (optional)") - leftIconName: "title" - onAccepted: root.submitAndClose() - width: addTitleBtn.visible ? parent.width - addTitleBtn.width - Theme.spacingS : parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - enabled: root.visible - } - - DankActionButton { - id: addTitleBtn - width: root.inputFieldHeight - height: root.inputFieldHeight - circular: false - iconName: "add" - iconSize: 16 - iconColor: Theme.surfaceVariantText - visible: !root.isEditMode && !!root.targetWindow?.title - tooltipText: I18n.tr("Add Title") - tooltipSide: "left" - onClicked: { - if (!root.targetWindow?.title) - return; - titleInput.text = isMango ? root.targetWindow.title : "^" + root.targetWindow.title + "$"; - } - } - } - - StyledText { - width: parent.width - visible: root.isNiri - text: I18n.tr("The rule applies to any window matching one of these.") - font.pixelSize: Theme.fontSizeSmall - 1 - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - } - - Repeater { - model: extraMatchModel - - delegate: Row { - width: parent.width - spacing: Theme.spacingS - - DankTextField { - id: extraAppId - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: root.isNiri ? I18n.tr("App ID regex") : I18n.tr("Class regex") - leftIconName: "apps" - onAccepted: root.submitAndClose() - width: (parent.width - removeMatchBtn.width - Theme.spacingS * 2) / 2 - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - enabled: root.visible - text: rowAppId - onTextEdited: extraMatchModel.setProperty(index, "rowAppId", text) - } - - DankTextField { - id: extraTitle - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Title regex (optional)") - leftIconName: "title" - onAccepted: root.submitAndClose() - width: (parent.width - removeMatchBtn.width - Theme.spacingS * 2) / 2 - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - enabled: root.visible - text: rowTitle - onTextEdited: extraMatchModel.setProperty(index, "rowTitle", text) - } - - DankActionButton { - id: removeMatchBtn - width: root.inputFieldHeight - height: root.inputFieldHeight - circular: false - iconName: "close" - iconSize: 16 - iconColor: Theme.surfaceVariantText - Accessible.name: I18n.tr("Remove match") - tooltipSide: "left" - onClicked: extraMatchModel.remove(index) - } - } - } - - DankButton { - text: I18n.tr("Add match") - iconName: "add" - backgroundColor: "transparent" - textColor: Theme.primary - visible: root.isNiri - onClicked: extraMatchModel.append({ - "rowAppId": "", - "rowTitle": "" - }) - } - - SectionHeader { - title: I18n.tr("Match Conditions") - visible: isNiri || isHyprland - } - - StyledText { - width: parent.width - visible: isNiri || isHyprland - text: I18n.tr("Optional state-based conditions applied to the first match.") - font.pixelSize: Theme.fontSizeSmall - 1 - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - } - - Flow { - width: parent.width - spacing: Theme.spacingS - visible: isNiri || isHyprland - - MatchCond { - id: condFloating - label: I18n.tr("Floating", "adjective, window rule match condition for floating windows") - } - MatchCond { - id: condActive - label: I18n.tr("Active") - visible: isNiri - } - MatchCond { - id: condFocused - label: I18n.tr("Focused", "adjective, window rule match condition for the focused window") - visible: isNiri - } - MatchCond { - id: condActiveInColumn - label: I18n.tr("Active in column") - visible: isNiri - } - MatchCond { - id: condCastTarget - label: I18n.tr("Cast target") - visible: isNiri - } - MatchCond { - id: condUrgent - label: I18n.tr("Urgent", "adjective, window rule match condition for windows requesting attention") - visible: isNiri - } - MatchCond { - id: condAtStartup - label: I18n.tr("At startup") - visible: isNiri - } - MatchCond { - id: condXwayland - label: "XWayland" - visible: isHyprland - } - MatchCond { - id: condFullscreen - label: I18n.tr("Fullscreen", "adjective, window rule match condition for fullscreen windows") - visible: isHyprland - } - MatchCond { - id: condPinned - label: I18n.tr("Pinned", "adjective, state of a pinned window, clipboard entry or item") - visible: isHyprland - } - MatchCond { - id: condInitialised - label: I18n.tr("Initialised", "adjective, hyprland window rule match condition") - visible: isHyprland - } - } - - SectionHeader { - title: I18n.tr("Window Opening") - } - - Flow { - width: parent.width - spacing: Theme.spacingL - - TriCheckboxRow { - id: floatingCond - label: I18n.tr("Float") - visible: isNiri - } - CheckboxRow { - id: floatingToggle - label: I18n.tr("Float") - visible: !isNiri - } - CheckboxRow { - id: maximizedToggle - label: I18n.tr("Maximize", "verb, window rule action to open the window maximized") - visible: !isMango - } - CheckboxRow { - id: fullscreenToggle - label: I18n.tr("Fullscreen", "window rule action, open the window fullscreen") - } - CheckboxRow { - id: maximizedToEdgesToggle - label: I18n.tr("Max edges", "window rule action, open maximized to screen edges") - visible: isNiri - } - CheckboxRow { - id: openFocusedToggle - label: I18n.tr("Focus", "verb, window rule action to focus the window when it opens") - visible: isNiri - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri || isHyprland - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - DankTextField { - id: outputInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Output", "noun, display output a window opens on, window rule field") - leftIconName: "monitor" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "HDMI-A-1" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - DankTextField { - id: workspaceInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Workspace") - leftIconName: "view_module" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "chat" - enabled: root.visible - } - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - DankTextField { - id: columnWidthInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Column Width") - leftIconName: "width" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "800" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - DankTextField { - id: windowHeightInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Window Height") - leftIconName: "height" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "600" - enabled: root.visible - } - } - } - - SectionHeader { - title: I18n.tr("Dynamic Properties") - visible: isNiri || isHyprland - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri || isHyprland - - CheckboxRow { - id: opacityEnabled - label: I18n.tr("Opacity") - anchors.verticalCenter: parent.verticalCenter - } - - DankSlider { - id: opacitySlider - wheelEnabled: false - width: Math.max(0, parent.width - opacityEnabled.width - parent.spacing) - Accessible.name: opacityEnabled.label - minimum: 10 - maximum: 100 - value: 100 - enabled: opacityEnabled.checked - } - } - - Flow { - width: parent.width - spacing: Theme.spacingL - visible: isNiri - - CheckboxRow { - id: vrrToggle - label: I18n.tr("VRR On-Demand") - } - CheckboxRow { - id: clipToGeometryToggle - label: I18n.tr("Clip to Geometry") - } - CheckboxRow { - id: tiledStateToggle - label: I18n.tr("Tiled State") - } - CheckboxRow { - id: drawBorderBgToggle - label: I18n.tr("Border with Background") - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - StyledText { - text: I18n.tr("Block Out From") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - DankDropdown { - id: blockOutDropdown - width: parent.width - dropdownWidth: parent.width - compactMode: true - options: ["", "screencast", "screen-capture"] - emptyText: I18n.tr("None") - } - } - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - StyledText { - text: I18n.tr("Column Display") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - DankDropdown { - id: columnDisplayDropdown - width: parent.width - dropdownWidth: parent.width - compactMode: true - options: ["", "tabbed"] - emptyText: I18n.tr("Normal") - } - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri - - CheckboxRow { - id: scrollFactorEnabled - label: I18n.tr("Scroll Factor") - anchors.verticalCenter: parent.verticalCenter - } - - DankSlider { - id: scrollFactorSlider - wheelEnabled: false - width: Math.max(0, parent.width - scrollFactorEnabled.width - parent.spacing) - Accessible.name: scrollFactorEnabled.label - minimum: 10 - maximum: 200 - value: 100 - enabled: scrollFactorEnabled.checked - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri || isHyprland - - CheckboxRow { - id: cornerRadiusEnabled - label: I18n.tr("Corner radius") - anchors.verticalCenter: parent.verticalCenter - } - - DankSlider { - id: cornerRadiusSlider - wheelEnabled: false - width: Math.max(0, parent.width - cornerRadiusEnabled.width - parent.spacing) - Accessible.name: cornerRadiusEnabled.label - minimum: 0 - maximum: 24 - value: 12 - enabled: cornerRadiusEnabled.checked - } - } - - SectionHeader { - title: I18n.tr("Background Effect") - visible: isNiri - } - - StyledText { - width: parent.width - visible: isNiri - text: I18n.tr("Xray blurs only the wallpaper (efficient) and is the default when Blur is on. Set Xray to Off for regular full blur of everything beneath the window (more expensive).") - font.pixelSize: Theme.fontSizeSmall - 1 - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - } - - Flow { - width: parent.width - spacing: Theme.spacingS - visible: isNiri - - MatchCond { - id: blurCond - label: I18n.tr("Blur", "noun, background blur effect option") - unsetLabel: I18n.tr("Inherit") - onTriStateChanged: { - if (triState === 2) - xrayCond.triState = 0; - } - } - MatchCond { - id: xrayCond - label: I18n.tr("X-Ray", "window rule background xray effect option") - unsetLabel: I18n.tr("Inherit") - readOnly: blurCond.triState === 2 - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri - - CheckboxRow { - id: noiseEnabled - label: I18n.tr("Noise", "window rule background noise effect checkbox") - anchors.verticalCenter: parent.verticalCenter - } - - DankSlider { - id: noiseSlider - wheelEnabled: false - width: Math.max(0, parent.width - noiseEnabled.width - parent.spacing) - Accessible.name: noiseEnabled.label - minimum: 0 - maximum: 100 - value: 5 - enabled: noiseEnabled.checked - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri - - CheckboxRow { - id: saturationEnabled - label: I18n.tr("Saturation", "window rule background color saturation checkbox") - anchors.verticalCenter: parent.verticalCenter - } - - DankSlider { - id: saturationSlider - wheelEnabled: false - width: Math.max(0, parent.width - saturationEnabled.width - parent.spacing) - Accessible.name: saturationEnabled.label - minimum: 0 - maximum: 200 - value: 100 - enabled: saturationEnabled.checked - } - } - - SectionHeader { - title: I18n.tr("Floating Position") - visible: isNiri - } - - StyledText { - width: parent.width - visible: isNiri - text: I18n.tr("Initial position for floating windows. Set both X and Y; anchor controls which corner/edge they're relative to.") - font.pixelSize: Theme.fontSizeSmall - 1 - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri - - Column { - width: (parent.width - Theme.spacingM * 2) / 3 - spacing: Theme.spacingXS - - DankTextField { - id: floatingXInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: "X" - leftIconName: "open_with" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "px" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM * 2) / 3 - spacing: Theme.spacingXS - - DankTextField { - id: floatingYInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: "Y" - leftIconName: "open_with" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "px" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM * 2) / 3 - spacing: Theme.spacingXS - - StyledText { - text: I18n.tr("Anchor", "noun, screen corner or edge a floating window position is relative to") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - DankDropdown { - id: floatingRelativeDropdown - width: parent.width - dropdownWidth: parent.width - compactMode: true - options: ["top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"] - } - } - } - - SectionHeader { - title: I18n.tr("Size Constraints") - visible: isNiri || isHyprland - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isNiri || isHyprland - - Column { - width: (parent.width - Theme.spacingM * 3) / 4 - spacing: Theme.spacingXS - - DankTextField { - id: minWidthInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Min W") - leftIconName: "width" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "px" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM * 3) / 4 - spacing: Theme.spacingXS - - DankTextField { - id: maxWidthInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Max W") - leftIconName: "width" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "px" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM * 3) / 4 - spacing: Theme.spacingXS - - DankTextField { - id: minHeightInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Min H") - leftIconName: "height" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "px" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM * 3) / 4 - spacing: Theme.spacingXS - - DankTextField { - id: maxHeightInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Max H") - leftIconName: "height" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "px" - enabled: root.visible - } - } - } - - SectionHeader { - title: I18n.tr("Hyprland Options") - visible: isHyprland - } - - Flow { - width: parent.width - spacing: Theme.spacingL - visible: isHyprland - - CheckboxRow { - id: tileToggle - label: I18n.tr("Tile") - } - CheckboxRow { - id: noFocusToggle - label: I18n.tr("No focus") - } - CheckboxRow { - id: noBorderToggle - label: I18n.tr("No border") - } - CheckboxRow { - id: noShadowToggle - label: I18n.tr("No shadow") - } - CheckboxRow { - id: noDimToggle - label: I18n.tr("No dim") - } - CheckboxRow { - id: noBlurToggle - label: I18n.tr("No blur") - } - CheckboxRow { - id: noAnimToggle - label: I18n.tr("No anim") - } - CheckboxRow { - id: noRoundingToggle - label: I18n.tr("No Rounding") - } - CheckboxRow { - id: pinToggle - label: I18n.tr("Pin", "verb, keep an item pinned in place") - } - CheckboxRow { - id: opaqueToggle - label: I18n.tr("Opaque", "adjective, window rule checkbox forcing an opaque window") - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isHyprland - - Column { - width: (parent.width - Theme.spacingM * 3) / 4 - spacing: Theme.spacingXS - - DankTextField { - id: moveXInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: "X" - leftIconName: "open_with" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "0" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM * 3) / 4 - spacing: Theme.spacingXS - - DankTextField { - id: moveYInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: "Y" - leftIconName: "open_with" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "0" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM * 3) / 4 - spacing: Theme.spacingXS - - DankTextField { - id: sizeWInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("W") - leftIconName: "width" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "800" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM * 3) / 4 - spacing: Theme.spacingXS - - DankTextField { - id: sizeHInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("H", "abbreviation of height, window rule size field label") - leftIconName: "height" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "600" - enabled: root.visible - } - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isHyprland - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - DankTextField { - id: monitorInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Monitor", "noun, display output field in window rule editor") - leftIconName: "monitor" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "DP-1" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - DankTextField { - id: hyprWorkspaceInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Workspace") - leftIconName: "view_module" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "1" - enabled: root.visible - } - } - } - - SectionHeader { - title: I18n.tr("Mango Options") - visible: isMango - } - - Flow { - width: parent.width - spacing: Theme.spacingL - visible: isMango - - CheckboxRow { - id: mangoNoBlurToggle - label: I18n.tr("No blur") - } - CheckboxRow { - id: mangoNoBorderToggle - label: I18n.tr("No border") - } - CheckboxRow { - id: mangoNoShadowToggle - label: I18n.tr("No shadow") - } - CheckboxRow { - id: mangoNoRoundingToggle - label: I18n.tr("No Rounding") - } - CheckboxRow { - id: mangoNoAnimToggle - label: I18n.tr("No anim") - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isMango - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - DankTextField { - id: mangoTagsInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Tags", "noun, mango compositor workspace tags field in window rule editor") - leftIconName: "label" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "1" - enabled: root.visible - } - } - - Column { - width: (parent.width - Theme.spacingM) / 2 - spacing: Theme.spacingXS - - DankTextField { - id: mangoMonitorInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Monitor") - leftIconName: "monitor" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "HDMI-A-1" - enabled: root.visible - } - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - visible: isMango - - Column { - width: parent.width - spacing: Theme.spacingXS - - DankTextField { - id: mangoSizeInput - outlined: true - controlHeight: Theme.fieldHeightLarge - labelText: I18n.tr("Size") - leftIconName: "aspect_ratio" - onAccepted: root.submitAndClose() - width: parent.width - font.pixelSize: Theme.fontSizeSmall - textColor: Theme.surfaceText - placeholderText: "800x600" - enabled: root.visible - } - } - } - - Item { - width: 1 - height: Theme.spacingM - } + onRuleSubmitted: { + root.ruleSubmitted(); + root.hide(); } - - actions: [ - DankButton { - maximumWidth: ruleDialog.actionWidth - wrapText: true - text: I18n.tr("Cancel") - backgroundColor: "transparent" - textColor: Theme.primary - onClicked: root.hide() - }, - DankButton { - maximumWidth: ruleDialog.actionWidth - wrapText: true - text: root.submitting ? I18n.tr("Saving...") : (root.isEditMode ? I18n.tr("Update", "verb, button saving changes to an existing window rule") : I18n.tr("Create", "verb, button creating a new window rule or display profile")) - enabled: !root.submitting - busy: root.submitting - onClicked: root.submitAndClose() - } - ] } FloatingWindowControls { diff --git a/quickshell/Modules/Settings/WindowRuleEditorDialog.qml b/quickshell/Modules/Settings/WindowRuleEditorDialog.qml new file mode 100644 index 000000000..74833eff7 --- /dev/null +++ b/quickshell/Modules/Settings/WindowRuleEditorDialog.qml @@ -0,0 +1,1203 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +DankDialog { + id: root + + property var editingRule: null + property var targetWindow: null + property bool submitting: false + readonly property bool isEditMode: editingRule !== null + readonly property bool isNiri: CompositorService.isNiri + readonly property bool isHyprland: CompositorService.isHyprland + readonly property bool isMango: CompositorService.isMango + readonly property bool fieldsEnabled: !submitting + + property int floatingTri: 0 + property var openingFlags: [] + property var dynamicFlags: [] + property var hyprFlags: [] + property var mangoFlags: [] + property bool opacityOn: false + property int opacityValue: 100 + property bool scrollFactorOn: false + property int scrollFactorValue: 100 + property bool cornerRadiusOn: false + property int cornerRadiusValue: 12 + property bool noiseOn: false + property int noiseValue: 5 + property bool saturationOn: false + property int saturationValue: 100 + property string blockOutValue: "" + property string columnDisplayValue: "" + property string floatingRelativeValue: "top-left" + + readonly property var triLabels: [I18n.tr("Default"), I18n.tr("On"), I18n.tr("Off")] + readonly property var blockOutOptions: ["", "screencast", "screen-capture"] + readonly property var columnDisplayOptions: ["", "tabbed"] + readonly property var anchorOptions: ["top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"] + readonly property var openingOptions: { + const options = []; + if (!isNiri) + options.push({ + "label": I18n.tr("Float"), + "value": "float" + }); + if (!isMango) + options.push({ + "label": I18n.tr("Maximize", "verb, window rule action to open the window maximized"), + "value": "maximize" + }); + options.push({ + "label": I18n.tr("Fullscreen", "window rule action, open the window fullscreen"), + "value": "fullscreen" + }); + if (isNiri) { + options.push({ + "label": I18n.tr("Max edges", "window rule action, open maximized to screen edges"), + "value": "maxEdges" + }); + options.push({ + "label": I18n.tr("Focus", "verb, window rule action to focus the window when it opens"), + "value": "focus" + }); + } + return options; + } + readonly property var dynamicOptions: [ + { + "label": I18n.tr("VRR On-Demand"), + "value": "vrr" + }, + { + "label": I18n.tr("Clip to Geometry"), + "value": "clip" + }, + { + "label": I18n.tr("Tiled State"), + "value": "tiled" + }, + { + "label": I18n.tr("Border with Background"), + "value": "borderBg" + } + ] + readonly property var hyprOptions: [ + { + "label": I18n.tr("Tile"), + "value": "tile" + }, + { + "label": I18n.tr("No focus"), + "value": "nofocus" + }, + { + "label": I18n.tr("No border"), + "value": "noborder" + }, + { + "label": I18n.tr("No shadow"), + "value": "noshadow" + }, + { + "label": I18n.tr("No dim"), + "value": "nodim" + }, + { + "label": I18n.tr("No blur"), + "value": "noblur" + }, + { + "label": I18n.tr("No anim"), + "value": "noanim" + }, + { + "label": I18n.tr("No Rounding"), + "value": "norounding" + }, + { + "label": I18n.tr("Pin", "verb, keep an item pinned in place"), + "value": "pin" + }, + { + "label": I18n.tr("Opaque", "adjective, window rule checkbox forcing an opaque window"), + "value": "opaque" + } + ] + readonly property var mangoOptions: [ + { + "label": I18n.tr("No blur"), + "value": "noblur" + }, + { + "label": I18n.tr("No border"), + "value": "noborder" + }, + { + "label": I18n.tr("No shadow"), + "value": "noshadow" + }, + { + "label": I18n.tr("No Rounding"), + "value": "norounding" + }, + { + "label": I18n.tr("No anim"), + "value": "noanim" + } + ] + + signal ruleSubmitted + + embedded: nativeWindow + opened: nativeWindow + maximumWidth: SettingsMetrics.formDialogWidth + surfaceColor: Theme.hostSurface + title: isEditMode ? I18n.tr("Edit Window Rule") : I18n.tr("New Window Rule") + closeEnabled: !submitting + acceptEnabled: !submitting + onAccepted: submit() + + ListModel { + id: extraMatchModel + } + + function show(window) { + editingRule = null; + targetWindow = window || null; + resetForm(); + if (targetWindow?.appId) { + nameInput.text = targetWindow.appId; + appIdInput.text = isMango ? targetWindow.appId : "^" + targetWindow.appId + "$"; + } + present(); + } + + function showEdit(rule) { + if (!rule) { + show(); + return; + } + editingRule = rule; + targetWindow = null; + resetForm(); + populateForm(rule); + present(); + } + + function showCopy(rule) { + if (!rule) { + show(); + return; + } + editingRule = null; + targetWindow = null; + resetForm(); + populateForm(rule); + present(); + } + + function present() { + opened = true; + forceActiveFocus(); + nameInput.forceActiveFocus(); + } + + function flagSet(flags, value, checked) { + const next = flags.filter(flag => flag !== value); + if (checked) + next.push(value); + return next; + } + + function triFromBool(value) { + if (value === true) + return 1; + if (value === false) + return 2; + return 0; + } + + function applyCond(target, key, triState) { + if (triState === 1) + target[key] = true; + else if (triState === 2) + target[key] = false; + } + + function blockOutLabel(value) { + return value === "" ? I18n.tr("None") : value; + } + + function columnDisplayLabel(value) { + return value === "" ? I18n.tr("Normal") : value; + } + + function resetForm() { + nameInput.text = ""; + appIdInput.text = ""; + titleInput.text = ""; + extraMatchModel.clear(); + for (const cond of matchConditions) + cond.triState = 0; + floatingTri = 0; + openingFlags = []; + dynamicFlags = []; + hyprFlags = []; + mangoFlags = []; + opacityOn = false; + opacityValue = 100; + scrollFactorOn = false; + scrollFactorValue = 100; + cornerRadiusOn = false; + cornerRadiusValue = 12; + noiseOn = false; + noiseValue = 5; + saturationOn = false; + saturationValue = 100; + blockOutValue = ""; + columnDisplayValue = ""; + floatingRelativeValue = "top-left"; + blurCond.triState = 0; + xrayCond.triState = 0; + for (const field of [outputInput, workspaceInput, columnWidthInput, windowHeightInput, floatingXInput, floatingYInput, minWidthInput, maxWidthInput, minHeightInput, maxHeightInput, moveXInput, moveYInput, sizeWInput, sizeHInput, monitorInput, hyprWorkspaceInput, mangoTagsInput, mangoMonitorInput, mangoSizeInput]) + field.text = ""; + } + + function populateForm(rule) { + nameInput.text = rule.name || ""; + const matchList = (rule.matches && rule.matches.length > 0) ? rule.matches : [rule.matchCriteria || {}]; + const match = matchList[0] || {}; + appIdInput.text = match.appId || ""; + titleInput.text = match.title || ""; + for (let i = 1; i < matchList.length; i++) + extraMatchModel.append({ + "rowAppId": matchList[i].appId || "", + "rowTitle": matchList[i].title || "" + }); + for (const cond of matchConditions) + cond.triState = triFromBool(match[cond.key]); + + const actions = rule.actions || {}; + const flags = []; + if (!isNiri && actions.openFloating) + flags.push("float"); + if (actions.openMaximized) + flags.push("maximize"); + if (actions.openFullscreen) + flags.push("fullscreen"); + if (actions.openMaximizedToEdges) + flags.push("maxEdges"); + if (actions.openFocused) + flags.push("focus"); + openingFlags = flags; + floatingTri = triFromBool(actions.openFloating); + + const dynamic = []; + if (actions.variableRefreshRate) + dynamic.push("vrr"); + if (actions.clipToGeometry) + dynamic.push("clip"); + if (actions.tiledState) + dynamic.push("tiled"); + if (actions.drawBorderWithBackground) + dynamic.push("borderBg"); + dynamicFlags = dynamic; + + opacityOn = actions.opacity !== undefined && actions.opacity !== null; + opacityValue = opacityOn ? Math.round(actions.opacity * 100) : 100; + scrollFactorOn = actions.scrollFactor !== undefined && actions.scrollFactor !== null; + scrollFactorValue = scrollFactorOn ? Math.round(actions.scrollFactor * 100) : 100; + cornerRadiusOn = actions.cornerRadius !== undefined && actions.cornerRadius !== null; + cornerRadiusValue = cornerRadiusOn ? actions.cornerRadius : 12; + noiseOn = actions.backgroundNoise !== undefined && actions.backgroundNoise !== null; + noiseValue = noiseOn ? Math.round(actions.backgroundNoise * 100) : 5; + saturationOn = actions.backgroundSaturation !== undefined && actions.backgroundSaturation !== null; + saturationValue = saturationOn ? Math.round(actions.backgroundSaturation * 100) : 100; + blockOutValue = actions.blockOutFrom || ""; + columnDisplayValue = actions.defaultColumnDisplay || ""; + blurCond.triState = triFromBool(actions.backgroundBlur); + xrayCond.triState = triFromBool(actions.backgroundXray); + + outputInput.text = actions.openOnOutput || ""; + workspaceInput.text = actions.openOnWorkspace || ""; + columnWidthInput.text = actions.defaultColumnWidth || ""; + windowHeightInput.text = actions.defaultWindowHeight || ""; + floatingXInput.text = (actions.defaultFloatingX !== undefined && actions.defaultFloatingX !== null) ? String(actions.defaultFloatingX) : ""; + floatingYInput.text = (actions.defaultFloatingY !== undefined && actions.defaultFloatingY !== null) ? String(actions.defaultFloatingY) : ""; + floatingRelativeValue = actions.defaultFloatingRelativeTo || "top-left"; + minWidthInput.text = actions.minWidth !== undefined ? String(actions.minWidth) : ""; + maxWidthInput.text = actions.maxWidth !== undefined ? String(actions.maxWidth) : ""; + minHeightInput.text = actions.minHeight !== undefined ? String(actions.minHeight) : ""; + maxHeightInput.text = actions.maxHeight !== undefined ? String(actions.maxHeight) : ""; + + hyprFlags = hyprOptions.map(option => option.value).filter(value => actions[value]); + moveXInput.text = actions.moveX || ""; + moveYInput.text = actions.moveY || ""; + sizeWInput.text = actions.sizeWidth || ""; + sizeHInput.text = actions.sizeHeight || ""; + monitorInput.text = actions.monitor || ""; + hyprWorkspaceInput.text = actions.workspace || ""; + + mangoFlags = mangoOptions.map(option => option.value).filter(value => actions[value]); + mangoTagsInput.text = actions.workspace || ""; + mangoMonitorInput.text = actions.monitor || ""; + mangoSizeInput.text = (actions.sizeWidth && actions.sizeHeight) ? actions.sizeWidth + "x" + actions.sizeHeight : ""; + } + + function collectMatches() { + const matchCriteria = {}; + if (appIdInput.text.trim()) + matchCriteria.appId = appIdInput.text.trim(); + if (titleInput.text.trim()) + matchCriteria.title = titleInput.text.trim(); + for (const cond of matchConditions) { + if (cond.visible) + applyCond(matchCriteria, cond.key, cond.triState); + } + const matches = []; + if (Object.keys(matchCriteria).length > 0) + matches.push(matchCriteria); + if (!isNiri) + return { + matchCriteria, + matches + }; + for (let i = 0; i < extraMatchModel.count; i++) { + const row = extraMatchModel.get(i); + const extra = {}; + if ((row.rowAppId || "").trim()) + extra.appId = row.rowAppId.trim(); + if ((row.rowTitle || "").trim()) + extra.title = row.rowTitle.trim(); + if (Object.keys(extra).length > 0) + matches.push(extra); + } + return { + matchCriteria, + matches + }; + } + + function collectActions() { + const actions = {}; + const has = value => openingFlags.includes(value); + if (opacityOn) + actions.opacity = opacityValue / 100; + if (isNiri) + applyCond(actions, "openFloating", floatingTri); + else if (has("float")) + actions.openFloating = true; + if (has("maximize")) + actions.openMaximized = true; + if (has("maxEdges") && isNiri) + actions.openMaximizedToEdges = true; + if (has("fullscreen")) + actions.openFullscreen = true; + if (has("focus") && isNiri) + actions.openFocused = true; + if (outputInput.text.trim()) + actions.openOnOutput = outputInput.text.trim(); + if (workspaceInput.text.trim()) + actions.openOnWorkspace = workspaceInput.text.trim(); + if (cornerRadiusOn) + actions.cornerRadius = cornerRadiusValue; + + const minW = parseInt(minWidthInput.text); + const maxW = parseInt(maxWidthInput.text); + const minH = parseInt(minHeightInput.text); + const maxH = parseInt(maxHeightInput.text); + if (!isNaN(minW)) + actions.minWidth = minW; + if (!isNaN(maxW)) + actions.maxWidth = maxW; + if (!isNaN(minH)) + actions.minHeight = minH; + if (!isNaN(maxH)) + actions.maxHeight = maxH; + + if (isNiri) { + if (columnWidthInput.text.trim()) + actions.defaultColumnWidth = columnWidthInput.text.trim(); + if (windowHeightInput.text.trim()) + actions.defaultWindowHeight = windowHeightInput.text.trim(); + if (dynamicFlags.includes("vrr")) + actions.variableRefreshRate = true; + if (dynamicFlags.includes("clip")) + actions.clipToGeometry = true; + if (dynamicFlags.includes("tiled")) + actions.tiledState = true; + if (dynamicFlags.includes("borderBg")) + actions.drawBorderWithBackground = true; + if (blockOutValue) + actions.blockOutFrom = blockOutValue; + if (columnDisplayValue) + actions.defaultColumnDisplay = columnDisplayValue; + if (scrollFactorOn) + actions.scrollFactor = scrollFactorValue / 100; + applyCond(actions, "backgroundBlur", blurCond.triState); + applyCond(actions, "backgroundXray", xrayCond.triState); + if (noiseOn) + actions.backgroundNoise = noiseValue / 100; + if (saturationOn) + actions.backgroundSaturation = saturationValue / 100; + const floatX = parseInt(floatingXInput.text); + const floatY = parseInt(floatingYInput.text); + if (!isNaN(floatX) && !isNaN(floatY)) { + actions.defaultFloatingX = floatX; + actions.defaultFloatingY = floatY; + if (floatingRelativeValue !== "top-left") + actions.defaultFloatingRelativeTo = floatingRelativeValue; + } + } + + if (isHyprland) { + for (const flag of hyprFlags) + actions[flag] = true; + if (sizeWInput.text.trim()) + actions.sizeWidth = sizeWInput.text.trim(); + if (sizeHInput.text.trim()) + actions.sizeHeight = sizeHInput.text.trim(); + if (moveXInput.text.trim()) + actions.moveX = moveXInput.text.trim(); + if (moveYInput.text.trim()) + actions.moveY = moveYInput.text.trim(); + if (monitorInput.text.trim()) + actions.monitor = monitorInput.text.trim(); + if (hyprWorkspaceInput.text.trim()) + actions.workspace = hyprWorkspaceInput.text.trim(); + } + + if (isMango) { + for (const flag of mangoFlags) + actions[flag] = true; + if (mangoTagsInput.text.trim()) + actions.workspace = mangoTagsInput.text.trim(); + if (mangoMonitorInput.text.trim()) + actions.monitor = mangoMonitorInput.text.trim(); + const parts = mangoSizeInput.text.trim().split(/x/i); + if (mangoSizeInput.text.trim() && parts.length === 2) { + actions.sizeWidth = parts[0].trim(); + actions.sizeHeight = parts[1].trim(); + } + } + return actions; + } + + function submit() { + if (submitting) + return; + const { + matchCriteria, + matches + } = collectMatches(); + const ruleData = { + name: nameInput.text.trim() || matchCriteria.appId || I18n.tr("Rule", "noun, fallback name for an unnamed window rule"), + matchCriteria: matchCriteria, + actions: collectActions(), + enabled: true + }; + if (isNiri && extraMatchModel.count > 0) + ruleData.matches = matches; + // No exclude editor yet; carry existing excludes through so an edit doesn't delete them (#2996) + if (isEditMode && (editingRule.excludes?.length ?? 0) > 0) + ruleData.excludes = editingRule.excludes; + + const compositor = CompositorService.compositor; + const ruleJson = JSON.stringify(ruleData); + const args = isEditMode ? [Proc.dmsBin, "config", "windowrules", "update", compositor, editingRule.id, ruleJson] : [Proc.dmsBin, "config", "windowrules", "add", compositor, ruleJson]; + submitting = true; + Proc.runCommand(isEditMode ? "update-windowrule" : "add-windowrule", args, (output, exitCode) => { + root.submitting = false; + if (exitCode !== 0) + return; + if (CompositorService.isNiri) + NiriService.validate(); + if (CompositorService.isMango) + MangoService.reloadConfig(); + root.ruleSubmitted(); + }); + } + + component MatchCond: StyledButton { + id: mc + + property string label: "" + property string key: "" + property int triState: 0 + property string unsetLabel: I18n.tr("Default") + readonly property var stateText: [mc.unsetLabel, "true", "false"] + readonly property var stateColor: [Theme.surfaceVariantText, Theme.primary, Theme.error] + + width: condRow.implicitWidth + Theme.spacingM * 2 + height: Theme.buttonHeightXS + radius: Theme.cornerRadiusS + color: enabled ? Theme.chipSurface : Theme.onSurface_12 + border.width: Theme.outlineWidth + border.color: !enabled ? "transparent" : mc.triState === 0 ? Theme.outlineVariant : mc.stateColor[mc.triState] + enabled: root.fieldsEnabled + Accessible.role: Accessible.Button + Accessible.name: label + ": " + stateText[triState] + onClicked: triState = (triState + 1) % 3 + + Row { + id: condRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: mc.label + font.pixelSize: Theme.fontSizeSmall + color: mc.enabled ? Theme.onSurface : Theme.onSurface_38 + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: stateBadge.implicitWidth + Theme.spacingS * 2 + height: stateBadge.implicitHeight + Theme.spacingXXS * 2 + radius: Theme.fullRadius(width, height) + color: mc.enabled ? Theme.withAlpha(mc.stateColor[mc.triState], Theme.stateLayerPressed) : "transparent" + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: stateBadge + anchors.centerIn: parent + text: mc.stateText[mc.triState] + font.pixelSize: Theme.fontSizeSmall + font.weight: Theme.fontWeightMedium + color: mc.enabled ? mc.stateColor[mc.triState] : Theme.onSurface_38 + } + } + } + + StateLayer { + control: mc + disabled: !mc.enabled + stateColor: Theme.primary + } + + FocusRing { + visible: mc.visualFocus + } + } + + component FieldRow: SettingsRow { + default property alias fields: fieldRow.data + + body: Row { + id: fieldRow + width: parent.width + spacing: Theme.spacingS + } + } + + component Field: DankTextField { + property int share: 1 + property int shares: 2 + + width: Math.round((parent.width - parent.spacing * (shares - 1)) * share / shares) + outlined: true + enabled: root.fieldsEnabled + onAccepted: root.submit() + } + + readonly property var matchConditions: [condFloating, condActive, condFocused, condActiveInColumn, condCastTarget, condUrgent, condAtStartup, condXwayland, condFullscreen, condPinned, condInitialised] + + actions: [ + DankButton { + text: I18n.tr("Cancel") + enabled: root.closeEnabled + backgroundColor: "transparent" + textColor: Theme.primary + onClicked: root.rejected() + }, + DankButton { + text: root.isEditMode ? I18n.tr("Save") : I18n.tr("Add") + enabled: root.acceptEnabled + busy: root.submitting + onClicked: root.submit() + } + ] + + SettingsGroup { + SettingsRow { + body: DankTextField { + id: nameInput + width: parent.width + outlined: true + leftIconName: "edit" + labelText: I18n.tr("Rule Name") + enabled: root.fieldsEnabled + onAccepted: root.submit() + } + } + } + + SettingsCard { + title: I18n.tr("Match Criteria") + headerActions: DankButton { + visible: root.isNiri + text: I18n.tr("Add match") + iconName: "add" + buttonHeight: Theme.buttonHeightXS + backgroundColor: "transparent" + textColor: Theme.primary + enabled: root.fieldsEnabled + onClicked: extraMatchModel.append({ + "rowAppId": "", + "rowTitle": "" + }) + } + + SettingsRow { + visible: root.isNiri + subtitle: I18n.tr("The rule applies to any window matching one of these.") + } + + SettingsRow { + body: DankTextField { + id: appIdInput + width: parent.width + outlined: true + leftIconName: "apps" + labelText: root.isMango ? I18n.tr("App ID (e.g. firefox)") : root.isHyprland ? I18n.tr("Class regex (e.g. ^firefox$)") : I18n.tr("App ID regex (e.g. ^firefox$)") + enabled: root.fieldsEnabled + onAccepted: root.submit() + } + } + + FieldRow { + Field { + id: titleInput + width: parent.width - (addTitle.visible ? addTitle.width + parent.spacing : 0) + leftIconName: "title" + labelText: root.isMango ? I18n.tr("Title (optional)") : I18n.tr("Title regex (optional)") + } + + DankActionButton { + id: addTitle + anchors.verticalCenter: parent.verticalCenter + iconName: "add" + visible: !root.isEditMode && !!root.targetWindow?.title + tooltipText: I18n.tr("Add Title") + onClicked: titleInput.text = root.isMango ? root.targetWindow.title : "^" + root.targetWindow.title + "$" + } + } + + Repeater { + model: extraMatchModel + + delegate: FieldRow { + id: extraRow + + required property int index + required property string rowAppId + required property string rowTitle + + Field { + width: Math.round((parent.width - parent.spacing * 2 - removeMatch.width) / 2) + leftIconName: "apps" + labelText: I18n.tr("App ID regex") + text: extraRow.rowAppId + onTextEdited: extraMatchModel.setProperty(extraRow.index, "rowAppId", text) + } + + Field { + width: Math.round((parent.width - parent.spacing * 2 - removeMatch.width) / 2) + leftIconName: "title" + labelText: I18n.tr("Title regex (optional)") + text: extraRow.rowTitle + onTextEdited: extraMatchModel.setProperty(extraRow.index, "rowTitle", text) + } + + DankActionButton { + id: removeMatch + anchors.verticalCenter: parent.verticalCenter + iconName: "close" + Accessible.name: I18n.tr("Remove match") + onClicked: extraMatchModel.remove(extraRow.index) + } + } + } + } + + SettingsCard { + title: I18n.tr("Match Conditions") + visible: root.isNiri || root.isHyprland + + SettingsRow { + subtitle: I18n.tr("Optional state-based conditions applied to the first match.") + } + + SettingsRow { + body: Flow { + width: parent.width + spacing: Theme.spacingS + + MatchCond { + id: condFloating + key: "isFloating" + label: I18n.tr("Floating", "adjective, window rule match condition for floating windows") + } + MatchCond { + id: condActive + key: "isActive" + label: I18n.tr("Active") + visible: root.isNiri + } + MatchCond { + id: condFocused + key: "isFocused" + label: I18n.tr("Focused", "adjective, window rule match condition for the focused window") + visible: root.isNiri + } + MatchCond { + id: condActiveInColumn + key: "isActiveInColumn" + label: I18n.tr("Active in column") + visible: root.isNiri + } + MatchCond { + id: condCastTarget + key: "isWindowCastTarget" + label: I18n.tr("Cast target") + visible: root.isNiri + } + MatchCond { + id: condUrgent + key: "isUrgent" + label: I18n.tr("Urgent", "adjective, window rule match condition for windows requesting attention") + visible: root.isNiri + } + MatchCond { + id: condAtStartup + key: "atStartup" + label: I18n.tr("At startup") + visible: root.isNiri + } + MatchCond { + id: condXwayland + key: "xwayland" + label: "XWayland" + visible: root.isHyprland + } + MatchCond { + id: condFullscreen + key: "fullscreen" + label: I18n.tr("Fullscreen", "adjective, window rule match condition for fullscreen windows") + visible: root.isHyprland + } + MatchCond { + id: condPinned + key: "pinned" + label: I18n.tr("Pinned", "adjective, state of a pinned window, clipboard entry or item") + visible: root.isHyprland + } + MatchCond { + id: condInitialised + key: "initialised" + label: I18n.tr("Initialised", "adjective, hyprland window rule match condition") + visible: root.isHyprland + } + } + } + } + + SettingsCard { + title: I18n.tr("Window Opening") + + SettingsButtonGroupRow { + visible: root.isNiri + text: I18n.tr("Float") + model: root.triLabels + currentIndex: root.floatingTri + enabled: root.fieldsEnabled + onSelectionChanged: (index, selected) => { + if (selected) + root.floatingTri = index; + } + } + + SettingsRow { + body: DankFilterChips { + width: parent.width + multiSelect: true + showCounts: false + enabled: root.fieldsEnabled + model: root.openingOptions + selectedValues: root.openingFlags + onSelectionToggled: (index, selected) => root.openingFlags = root.flagSet(root.openingFlags, root.openingOptions[index].value, selected) + } + } + + FieldRow { + visible: root.isNiri || root.isHyprland + + Field { + id: outputInput + leftIconName: "monitor" + labelText: I18n.tr("Output", "noun, display output a window opens on, window rule field") + placeholderText: "HDMI-A-1" + } + + Field { + id: workspaceInput + leftIconName: "view_module" + labelText: I18n.tr("Workspace") + placeholderText: "chat" + } + } + + FieldRow { + visible: root.isNiri + + Field { + id: columnWidthInput + leftIconName: "width" + labelText: I18n.tr("Column Width") + placeholderText: "800" + } + + Field { + id: windowHeightInput + leftIconName: "height" + labelText: I18n.tr("Window Height") + placeholderText: "600" + } + } + } + + SettingsCard { + title: I18n.tr("Dynamic Properties") + visible: root.isNiri || root.isHyprland + + SettingsToggleRow { + text: I18n.tr("Opacity") + checked: root.opacityOn + enabled: root.fieldsEnabled + onToggled: checked => root.opacityOn = checked + } + + SettingsSliderRow { + visible: root.opacityOn + text: I18n.tr("Opacity") + value: root.opacityValue + minimum: 10 + unit: "%" + enabled: root.fieldsEnabled + onSliderValueChanged: newValue => root.opacityValue = newValue + } + + SettingsToggleRow { + text: I18n.tr("Corner radius") + checked: root.cornerRadiusOn + enabled: root.fieldsEnabled + onToggled: checked => root.cornerRadiusOn = checked + } + + SettingsSliderRow { + visible: root.cornerRadiusOn + text: I18n.tr("Corner radius") + value: root.cornerRadiusValue + maximum: 24 + unit: "px" + enabled: root.fieldsEnabled + onSliderValueChanged: newValue => root.cornerRadiusValue = newValue + } + + SettingsToggleRow { + visible: root.isNiri + text: I18n.tr("Scroll Factor") + checked: root.scrollFactorOn + enabled: root.fieldsEnabled + onToggled: checked => root.scrollFactorOn = checked + } + + SettingsSliderRow { + visible: root.isNiri && root.scrollFactorOn + text: I18n.tr("Scroll Factor") + value: root.scrollFactorValue + minimum: 10 + maximum: 200 + unit: "%" + enabled: root.fieldsEnabled + onSliderValueChanged: newValue => root.scrollFactorValue = newValue + } + + SettingsRow { + visible: root.isNiri + + body: DankFilterChips { + width: parent.width + multiSelect: true + showCounts: false + enabled: root.fieldsEnabled + model: root.dynamicOptions + selectedValues: root.dynamicFlags + onSelectionToggled: (index, selected) => root.dynamicFlags = root.flagSet(root.dynamicFlags, root.dynamicOptions[index].value, selected) + } + } + + SettingsDropdownRow { + visible: root.isNiri + text: I18n.tr("Block Out From") + options: root.blockOutOptions.map(root.blockOutLabel) + currentValue: root.blockOutLabel(root.blockOutValue) + enabled: root.fieldsEnabled + onValueChanged: value => root.blockOutValue = root.blockOutOptions.find(option => root.blockOutLabel(option) === value) ?? "" + } + + SettingsDropdownRow { + visible: root.isNiri + text: I18n.tr("Column Display") + options: root.columnDisplayOptions.map(root.columnDisplayLabel) + currentValue: root.columnDisplayLabel(root.columnDisplayValue) + enabled: root.fieldsEnabled + onValueChanged: value => root.columnDisplayValue = root.columnDisplayOptions.find(option => root.columnDisplayLabel(option) === value) ?? "" + } + } + + SettingsCard { + title: I18n.tr("Background Effect") + visible: root.isNiri + + SettingsRow { + subtitle: I18n.tr("Xray blurs only the wallpaper (efficient) and is the default when Blur is on. Set Xray to Off for regular full blur of everything beneath the window (more expensive).") + } + + SettingsRow { + body: Flow { + width: parent.width + spacing: Theme.spacingS + + MatchCond { + id: blurCond + label: I18n.tr("Blur", "noun, background blur effect option") + unsetLabel: I18n.tr("Inherit") + } + + MatchCond { + id: xrayCond + label: I18n.tr("X-Ray", "window rule background xray effect option") + unsetLabel: I18n.tr("Inherit") + } + } + } + + SettingsToggleRow { + text: I18n.tr("Noise", "window rule background noise effect checkbox") + checked: root.noiseOn + enabled: root.fieldsEnabled + onToggled: checked => root.noiseOn = checked + } + + SettingsSliderRow { + visible: root.noiseOn + text: I18n.tr("Noise", "window rule background noise effect checkbox") + value: root.noiseValue + unit: "%" + enabled: root.fieldsEnabled + onSliderValueChanged: newValue => root.noiseValue = newValue + } + + SettingsToggleRow { + text: I18n.tr("Saturation", "window rule background color saturation checkbox") + checked: root.saturationOn + enabled: root.fieldsEnabled + onToggled: checked => root.saturationOn = checked + } + + SettingsSliderRow { + visible: root.saturationOn + text: I18n.tr("Saturation", "window rule background color saturation checkbox") + value: root.saturationValue + maximum: 200 + unit: "%" + enabled: root.fieldsEnabled + onSliderValueChanged: newValue => root.saturationValue = newValue + } + } + + SettingsCard { + title: I18n.tr("Floating Position") + visible: root.isNiri + + SettingsRow { + subtitle: I18n.tr("Initial position for floating windows. Set both X and Y; anchor controls which corner/edge they're relative to.") + } + + FieldRow { + Field { + id: floatingXInput + leftIconName: "open_with" + labelText: "X" + placeholderText: "px" + } + + Field { + id: floatingYInput + leftIconName: "open_with" + labelText: "Y" + placeholderText: "px" + } + } + + SettingsDropdownRow { + text: I18n.tr("Anchor", "noun, screen corner or edge a floating window position is relative to") + options: root.anchorOptions + currentValue: root.floatingRelativeValue + enabled: root.fieldsEnabled + onValueChanged: value => root.floatingRelativeValue = value + } + } + + SettingsCard { + title: I18n.tr("Size Constraints") + visible: root.isNiri || root.isHyprland + + FieldRow { + Field { + id: minWidthInput + leftIconName: "width" + labelText: I18n.tr("Min W") + placeholderText: "px" + } + + Field { + id: maxWidthInput + leftIconName: "width" + labelText: I18n.tr("Max W") + placeholderText: "px" + } + } + + FieldRow { + Field { + id: minHeightInput + leftIconName: "height" + labelText: I18n.tr("Min H") + placeholderText: "px" + } + + Field { + id: maxHeightInput + leftIconName: "height" + labelText: I18n.tr("Max H") + placeholderText: "px" + } + } + } + + SettingsCard { + title: I18n.tr("Hyprland Options") + visible: root.isHyprland + + SettingsRow { + body: DankFilterChips { + width: parent.width + multiSelect: true + showCounts: false + enabled: root.fieldsEnabled + model: root.hyprOptions + selectedValues: root.hyprFlags + onSelectionToggled: (index, selected) => root.hyprFlags = root.flagSet(root.hyprFlags, root.hyprOptions[index].value, selected) + } + } + + FieldRow { + Field { + id: moveXInput + leftIconName: "open_with" + labelText: "X" + placeholderText: "0" + } + + Field { + id: moveYInput + leftIconName: "open_with" + labelText: "Y" + placeholderText: "0" + } + } + + FieldRow { + Field { + id: sizeWInput + leftIconName: "width" + labelText: I18n.tr("W") + placeholderText: "800" + } + + Field { + id: sizeHInput + leftIconName: "height" + labelText: I18n.tr("H", "abbreviation of height, window rule size field label") + placeholderText: "600" + } + } + + FieldRow { + Field { + id: monitorInput + leftIconName: "monitor" + labelText: I18n.tr("Monitor", "noun, display output field in window rule editor") + placeholderText: "DP-1" + } + + Field { + id: hyprWorkspaceInput + leftIconName: "view_module" + labelText: I18n.tr("Workspace") + placeholderText: "1" + } + } + } + + SettingsCard { + title: I18n.tr("Mango Options") + visible: root.isMango + + SettingsRow { + body: DankFilterChips { + width: parent.width + multiSelect: true + showCounts: false + enabled: root.fieldsEnabled + model: root.mangoOptions + selectedValues: root.mangoFlags + onSelectionToggled: (index, selected) => root.mangoFlags = root.flagSet(root.mangoFlags, root.mangoOptions[index].value, selected) + } + } + + FieldRow { + Field { + id: mangoTagsInput + leftIconName: "view_module" + labelText: I18n.tr("Tags", "noun, mango compositor workspace tags field in window rule editor") + placeholderText: "1" + } + + Field { + id: mangoMonitorInput + leftIconName: "monitor" + labelText: I18n.tr("Monitor") + placeholderText: "HDMI-A-1" + } + } + + FieldRow { + Field { + id: mangoSizeInput + shares: 1 + leftIconName: "aspect_ratio" + labelText: I18n.tr("Size") + placeholderText: "800x600" + } + } + } +} diff --git a/quickshell/Modules/Settings/WindowRulesTab.qml b/quickshell/Modules/Settings/WindowRulesTab.qml index 60692f972..9e4efe1ce 100644 --- a/quickshell/Modules/Settings/WindowRulesTab.qml +++ b/quickshell/Modules/Settings/WindowRulesTab.qml @@ -243,48 +243,64 @@ Item { }); } + property bool editorOpen: false + property bool editorMounted: false + property var editorRequest: null + function openRuleModal(window) { - if (readOnly) { - showHyprlandReadOnlyWarning(); - return; - } - if (!PopoutService.windowRuleModalLoader) - return; - PopoutService.windowRuleModalLoader.active = true; - if (PopoutService.windowRuleModalLoader.item) { - PopoutService.windowRuleModalLoader.item.onRuleSubmitted.connect(loadWindowRules); - PopoutService.windowRuleModalLoader.item.show(window || null); - } + openEditor("new", window || null); } function editRule(rule) { - if (readOnly) { - showHyprlandReadOnlyWarning(); - return; - } - if (!PopoutService.windowRuleModalLoader) - return; - PopoutService.windowRuleModalLoader.active = true; - if (PopoutService.windowRuleModalLoader.item) { - PopoutService.windowRuleModalLoader.item.onRuleSubmitted.connect(loadWindowRules); - PopoutService.windowRuleModalLoader.item.showEdit(rule); - } + openEditor("edit", rule); } function copyRuleToDms(rule) { + openEditor("copy", rule); + } + + function openEditor(mode, payload) { if (readOnly) { showHyprlandReadOnlyWarning(); return; } - if (!PopoutService.windowRuleModalLoader) + if (editorOpen) return; - PopoutService.windowRuleModalLoader.active = true; - if (PopoutService.windowRuleModalLoader.item) { - PopoutService.windowRuleModalLoader.item.onRuleSubmitted.connect(loadWindowRules); - PopoutService.windowRuleModalLoader.item.showCopy(rule); + editorRequest = { + mode, + payload + }; + editorOpen = true; + editorMounted = true; + if (editorLoader.item) + presentEditor(); + } + + function presentEditor() { + const request = editorRequest; + if (!request) + return; + switch (request.mode) { + case "edit": + editorLoader.item.showEdit(request.payload); + return; + case "copy": + editorLoader.item.showCopy(request.payload); + return; + default: + editorLoader.item.show(request.payload); } } + function closeEditor() { + if (!editorOpen) + return; + editorOpen = false; + editorRequest = null; + if (editorLoader.item) + editorLoader.item.opened = false; + } + function showHyprlandReadOnlyWarning() { ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."), "dms setup", "hyprland-migration"); } @@ -649,4 +665,26 @@ Item { } } } + + Loader { + id: editorLoader + parent: root.parentModal?.modalFocusScope ?? root + anchors.fill: parent + z: 100 + active: root.editorMounted + onLoaded: root.presentEditor() + + sourceComponent: WindowRuleEditorDialog { + supportingText: I18n.tr("Changes save to %1", "keybind editor dialog hint, %1 is the binds file path").arg(root.dmsRulesFileName) + onRejected: root.closeEditor() + onRuleSubmitted: { + root.loadWindowRules(); + root.closeEditor(); + } + onActiveChanged: { + if (!active && !root.editorOpen) + root.editorMounted = false; + } + } + } }