mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-26 13:07:49 -04:00
* fix(hyprland): write window rules Hyprland 0.56 accepts, keep DMS windows opaque
The Lua window-rule writer emitted match keys and effects Hyprland 0.56
does not know: `floating`/`pinned`/`initialised` as match keys and
`noborder`/`norounding` as effects. It now writes `float`/`pin` and
`border_size = 0`/`rounding = 0`; the parser still reads the old
spellings, so existing files load and are rewritten on the next save.
"No Rounding" now suppresses the corner radius so a rule has one
`rounding` key.
The rule editor no longer offers fields the Hyprland writer drops
(Window Opening output/workspace, Size Constraints, the Initialised
match), and Save is disabled while only one half of a move or size
pair is filled. Every window-rule write now reloads the compositor
through one helper, which also covers Hyprland.
DMS windows draw their own translucent background, so any compositor
opacity rule or decoration opacity multiplied on top of it. DMS now
keeps a hidden rule for its own app id: `opaque = true` on Hyprland
(applied after rule and decoration opacity, independent of order, and
leaving the window's own alpha alone) and `opacity 1.0` on niri, which
the store keeps last. It is re-added on start when missing and hidden
from the rules list and the focused-window menu.
Compositor layout gains a Hyprland "Window opacity" slider writing
decoration.active_opacity/inactive_opacity; 100 leaves the user's own
config in charge.
HyprlandService.reloadConfig queues callbacks: Proc keeps one callback
per id, so a reload requested while the layout writer's reload was
pending dropped its callback and left layout generation stuck.
* fix(windowrules): cap corner radius at 20
Hyprland 0.56's Lua window_rule rejects rounding above 20 ("value 24
is more than the maximum of 20") and the rule loses its rounding. The
Hyprland writer now clamps to 20, and the editor slider stops at 20
for every compositor.
1214 lines
42 KiB
QML
1214 lines
42 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import qs.Common
|
|
import "../../Common/WindowRuleSize.js" as WindowRuleSize
|
|
import qs.Services
|
|
import qs.DCommon.Widgets
|
|
import qs.Widgets
|
|
import qs.Modules.Settings.Widgets
|
|
|
|
DDialog {
|
|
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
|
|
readonly property bool halfPair: isHyprland && (!!moveXInput.text.trim() !== !!moveYInput.text.trim() || !!sizeWInput.text.trim() !== !!sizeHInput.text.trim())
|
|
|
|
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"
|
|
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"]
|
|
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 && !halfPair
|
|
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";
|
|
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])
|
|
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 || "";
|
|
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";
|
|
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 (isNiri && outputInput.text.trim())
|
|
actions.openOnOutput = outputInput.text.trim();
|
|
if (isNiri && 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 (isNiri) {
|
|
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) {
|
|
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"))
|
|
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 || !acceptEnabled)
|
|
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;
|
|
CompositorService.reloadAfterWindowRuleWrite();
|
|
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: DTextField {
|
|
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()
|
|
}
|
|
|
|
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
|
|
|
|
DButtonGroup {
|
|
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]
|
|
|
|
actions: [
|
|
DButton {
|
|
text: I18n.tr("Cancel")
|
|
enabled: root.closeEnabled
|
|
backgroundColor: "transparent"
|
|
textColor: Theme.primary
|
|
onClicked: root.rejected()
|
|
},
|
|
DButton {
|
|
text: root.isEditMode ? I18n.tr("Save") : I18n.tr("Add")
|
|
enabled: root.acceptEnabled
|
|
busy: root.submitting
|
|
onClicked: root.submit()
|
|
}
|
|
]
|
|
|
|
SettingsGroup {
|
|
SettingsRow {
|
|
body: DTextField {
|
|
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: DButton {
|
|
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: DTextField {
|
|
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)")
|
|
}
|
|
|
|
DActionButton {
|
|
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)
|
|
}
|
|
|
|
DActionButton {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: I18n.tr("Window Opening")
|
|
|
|
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
|
|
onSelectionChanged: (index, selected) => {
|
|
if (selected)
|
|
root.floatingTri = index;
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
body: DFilterChips {
|
|
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
|
|
|
|
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
|
|
|
|
SizeField {
|
|
id: columnWidthInput
|
|
leftIconName: "width"
|
|
labelText: I18n.tr("Column Width")
|
|
pixelsPlaceholder: "800"
|
|
unit: root.columnWidthUnit
|
|
onUnitSelected: unit => root.columnWidthUnit = unit
|
|
}
|
|
|
|
SizeField {
|
|
id: windowHeightInput
|
|
leftIconName: "height"
|
|
labelText: I18n.tr("Window Height")
|
|
pixelsPlaceholder: "600"
|
|
unit: root.windowHeightUnit
|
|
onUnitSelected: unit => root.windowHeightUnit = unit
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: I18n.tr("Dynamic Properties")
|
|
visible: root.isNiri || root.isHyprland
|
|
|
|
SettingsToggleSliderRow {
|
|
text: I18n.tr("Opacity")
|
|
checked: root.opacityOn
|
|
value: root.opacityValue
|
|
minimum: 10
|
|
unit: "%"
|
|
enabled: root.fieldsEnabled
|
|
onToggled: checked => root.opacityOn = checked
|
|
onSliderValueChanged: newValue => root.opacityValue = newValue
|
|
}
|
|
|
|
SettingsToggleSliderRow {
|
|
text: I18n.tr("Corner radius")
|
|
checked: root.cornerRadiusOn
|
|
value: root.cornerRadiusValue
|
|
maximum: 20
|
|
unit: "px"
|
|
enabled: root.fieldsEnabled
|
|
onToggled: checked => root.cornerRadiusOn = checked
|
|
onSliderValueChanged: newValue => root.cornerRadiusValue = newValue
|
|
}
|
|
|
|
SettingsToggleSliderRow {
|
|
visible: root.isNiri
|
|
text: I18n.tr("Scroll Factor")
|
|
checked: root.scrollFactorOn
|
|
value: root.scrollFactorValue
|
|
minimum: 10
|
|
maximum: 200
|
|
unit: "%"
|
|
enabled: root.fieldsEnabled
|
|
onToggled: checked => root.scrollFactorOn = checked
|
|
onSliderValueChanged: newValue => root.scrollFactorValue = newValue
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.isNiri
|
|
|
|
body: DFilterChips {
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsToggleSliderRow {
|
|
text: I18n.tr("Noise", "window rule background noise effect checkbox")
|
|
checked: root.noiseOn
|
|
value: root.noiseValue
|
|
unit: "%"
|
|
enabled: root.fieldsEnabled
|
|
onToggled: checked => root.noiseOn = checked
|
|
onSliderValueChanged: newValue => root.noiseValue = newValue
|
|
}
|
|
|
|
SettingsToggleSliderRow {
|
|
text: I18n.tr("Saturation", "window rule background color saturation checkbox")
|
|
checked: root.saturationOn
|
|
value: root.saturationValue
|
|
maximum: 200
|
|
unit: "%"
|
|
enabled: root.fieldsEnabled
|
|
onToggled: checked => root.saturationOn = checked
|
|
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
|
|
|
|
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: DFilterChips {
|
|
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: DFilterChips {
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
}
|