diff --git a/quickshell/Common/WindowRuleSize.js b/quickshell/Common/WindowRuleSize.js new file mode 100644 index 000000000..738486241 --- /dev/null +++ b/quickshell/Common/WindowRuleSize.js @@ -0,0 +1,32 @@ +.pragma library + +const PERCENT = "%"; +const PIXELS = "px"; + +function parse(value) { + const parts = String(value || "").trim().split(/\s+/); + if (parts[0] !== "proportion") + return { + unit: PIXELS, + amount: parts[parts.length - 1] || "" + }; + const fraction = parseFloat(parts[1]); + return { + unit: PERCENT, + amount: isNaN(fraction) ? "" : String(Math.round(fraction * 10000) / 100) + }; +} + +function format(unit, amount) { + const number = parseFloat(amount); + if (isNaN(number) || number <= 0) + return ""; + if (unit === PERCENT) + return "proportion " + Math.round(number * 100) / 10000; + return "fixed " + Math.round(number); +} + +function label(value) { + const size = parse(value); + return size.amount + size.unit; +} diff --git a/quickshell/Modules/DankDash/Media/LyricsOverlay.qml b/quickshell/Modules/DankDash/Media/LyricsOverlay.qml index c8b29f1a5..3536a25d4 100644 --- a/quickshell/Modules/DankDash/Media/LyricsOverlay.qml +++ b/quickshell/Modules/DankDash/Media/LyricsOverlay.qml @@ -298,11 +298,13 @@ FocusScope { width: transcript.width height: vocals.implicitHeight + // Positioners relayout on polish, so the list would measure reused and rescaled rows at their old height. Column { id: vocals width: parent.width Repeater { + onItemAdded: vocals.forceLayout() model: root.controller.synced ? lyric.modelData.parts : [ { x: lyric.modelData, @@ -326,6 +328,7 @@ FocusScope { distance: lyric ? Math.abs(lyric.index - root.activeIndex) : 0 animationsEnabled: root.animationsEnabled smoothHighlight: root.smoothHighlight + onImplicitHeightChanged: vocals.forceLayout() inViewport: { if (!lyric) return false; diff --git a/quickshell/Modules/Settings/ThemeSurfacesTab.qml b/quickshell/Modules/Settings/ThemeSurfacesTab.qml index 8f7b55a63..659f43cc5 100644 --- a/quickshell/Modules/Settings/ThemeSurfacesTab.qml +++ b/quickshell/Modules/Settings/ThemeSurfacesTab.qml @@ -352,6 +352,7 @@ Column { tags: ["floating", "window", "tile", "tiling", "compositor", "rule", "niri", "hyprland", "mango"] settingKey: "dmsWindowsFloating" text: I18n.tr("Open floating") + description: I18n.tr("Adds a compositor window rule so DMS windows such as Settings open floating", "theme floating windows section, open floating toggle description") visible: windowRulesInclude.compositorSupported checked: CompositorService.dmsWindowFloatingActive modified: !checked diff --git a/quickshell/Modules/Settings/WindowRuleEditorDialog.qml b/quickshell/Modules/Settings/WindowRuleEditorDialog.qml index 74833eff7..0b18c2b31 100644 --- a/quickshell/Modules/Settings/WindowRuleEditorDialog.qml +++ b/quickshell/Modules/Settings/WindowRuleEditorDialog.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import qs.Common +import "../../Common/WindowRuleSize.js" as WindowRuleSize import qs.Services import qs.Widgets import qs.Modules.Settings.Widgets @@ -36,6 +37,8 @@ DankDialog { property string blockOutValue: "" property string columnDisplayValue: "" property string floatingRelativeValue: "top-left" + property string columnWidthUnit: WindowRuleSize.PIXELS + property string windowHeightUnit: WindowRuleSize.PIXELS readonly property var triLabels: [I18n.tr("Default"), I18n.tr("On"), I18n.tr("Off")] readonly property var blockOutOptions: ["", "screencast", "screen-capture"] @@ -263,6 +266,8 @@ DankDialog { blockOutValue = ""; columnDisplayValue = ""; floatingRelativeValue = "top-left"; + columnWidthUnit = WindowRuleSize.PIXELS; + windowHeightUnit = WindowRuleSize.PIXELS; 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]) @@ -326,8 +331,12 @@ DankDialog { outputInput.text = actions.openOnOutput || ""; workspaceInput.text = actions.openOnWorkspace || ""; - columnWidthInput.text = actions.defaultColumnWidth || ""; - windowHeightInput.text = actions.defaultWindowHeight || ""; + const columnWidth = WindowRuleSize.parse(actions.defaultColumnWidth); + const windowHeight = WindowRuleSize.parse(actions.defaultWindowHeight); + columnWidthUnit = columnWidth.unit; + windowHeightUnit = windowHeight.unit; + columnWidthInput.text = columnWidth.amount; + windowHeightInput.text = windowHeight.amount; 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"; @@ -422,10 +431,12 @@ DankDialog { actions.maxHeight = maxH; if (isNiri) { - if (columnWidthInput.text.trim()) - actions.defaultColumnWidth = columnWidthInput.text.trim(); - if (windowHeightInput.text.trim()) - actions.defaultWindowHeight = windowHeightInput.text.trim(); + const columnWidth = WindowRuleSize.format(columnWidthUnit, columnWidthInput.text); + const windowHeight = WindowRuleSize.format(windowHeightUnit, windowHeightInput.text); + if (columnWidth) + actions.defaultColumnWidth = columnWidth; + if (windowHeight) + actions.defaultWindowHeight = windowHeight; if (dynamicFlags.includes("vrr")) actions.variableRefreshRate = true; if (dynamicFlags.includes("clip")) @@ -606,6 +617,35 @@ DankDialog { onAccepted: root.submit() } + component SizeField: Field { + id: sizeField + + property string unit: WindowRuleSize.PIXELS + property string pixelsPlaceholder: "" + + signal unitSelected(string unit) + + placeholderText: unit === WindowRuleSize.PERCENT ? "50" : pixelsPlaceholder + rightAccessoryWidth: unitPicker.width + Theme.spacingS + + DankButtonGroup { + id: unitPicker + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + y: sizeField.containerTop + (sizeField.containerHeight - height) / 2 + size: "small" + checkEnabled: false + arrowKeysSelect: false + enabled: sizeField.enabled + model: [I18n.tr("px"), WindowRuleSize.PERCENT] + currentIndex: sizeField.unit === WindowRuleSize.PERCENT ? 1 : 0 + onSelectionChanged: (index, selected) => { + if (selected) + sizeField.unitSelected(index === 1 ? WindowRuleSize.PERCENT : WindowRuleSize.PIXELS); + } + } + } + readonly property var matchConditions: [condFloating, condActive, condFocused, condActiveInColumn, condCastTarget, condUrgent, condAtStartup, condXwayland, condFullscreen, condPinned, condInitialised] actions: [ @@ -814,6 +854,7 @@ DankDialog { SettingsButtonGroupRow { visible: root.isNiri text: I18n.tr("Float") + description: I18n.tr("Default leaves it to niri, which floats dialogs and fixed-size windows and tiles the rest", "window rule float option, explains the Default choice") model: root.triLabels currentIndex: root.floatingTri enabled: root.fieldsEnabled @@ -856,18 +897,22 @@ DankDialog { FieldRow { visible: root.isNiri - Field { + SizeField { id: columnWidthInput leftIconName: "width" labelText: I18n.tr("Column Width") - placeholderText: "800" + pixelsPlaceholder: "800" + unit: root.columnWidthUnit + onUnitSelected: unit => root.columnWidthUnit = unit } - Field { + SizeField { id: windowHeightInput leftIconName: "height" labelText: I18n.tr("Window Height") - placeholderText: "600" + pixelsPlaceholder: "600" + unit: root.windowHeightUnit + onUnitSelected: unit => root.windowHeightUnit = unit } } } diff --git a/quickshell/Modules/Settings/WindowRulesTab.qml b/quickshell/Modules/Settings/WindowRulesTab.qml index 669f109a5..4c8d7629a 100644 --- a/quickshell/Modules/Settings/WindowRulesTab.qml +++ b/quickshell/Modules/Settings/WindowRulesTab.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Wayland import qs.Common import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve +import "../../Common/WindowRuleSize.js" as WindowRuleSize import qs.Services import qs.Widgets import qs.Modules.Settings.Widgets @@ -73,6 +74,8 @@ Item { const label = root.actionLabels[k] || k; if (typeof a[k] === "boolean") return a[k] ? label : label + ": " + I18n.tr("Off"); + if (k === "defaultColumnWidth" || k === "defaultWindowHeight") + return label + ": " + WindowRuleSize.label(a[k]); return label + ": " + a[k]; }); } diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 267012cbd..3612dc6f7 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -1042,14 +1042,18 @@ "tabIndex": -1, "category": "Interface style", "keywords": [ + "adds", "authentication", + "compositor", "floating", "foreground", "interface", "notepad", "opacity", + "open", "override", "polkit", + "rule", "settings", "style", "tile", @@ -1058,7 +1062,8 @@ "window", "windows" ], - "page": "theme_surfaces" + "page": "theme_surfaces", + "description": "Adds a compositor window rule so DMS windows such as Settings open floating" }, { "section": "focusRingEnabled", @@ -1818,6 +1823,7 @@ "tabIndex": -1, "category": "Interface style", "keywords": [ + "adds", "compositor", "floating", "hyprland", @@ -1826,12 +1832,15 @@ "niri", "open", "rule", + "settings", "style", "tile", "tiling", - "window" + "window", + "windows" ], - "page": "theme_surfaces" + "page": "theme_surfaces", + "description": "Adds a compositor window rule so DMS windows such as Settings open floating" }, { "section": "notificationOverlayEnabled",