1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-13 06:33:30 -04:00

feat(window-rules): view & convert external rules to DMS

- Read and convert external compositor rules into editable DMS rules
- Preserve niri multi-match rules and add match editor
- niri background-effect (blur/xray/noise/saturation) support
This commit is contained in:
purian23
2026-06-03 08:59:51 -04:00
parent a34fda984d
commit d20aa3b80a
4 changed files with 1039 additions and 106 deletions
+441 -9
View File
@@ -20,6 +20,10 @@ FloatingWindow {
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)
@@ -31,6 +35,18 @@ FloatingWindow {
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;
floatingToggle.checked = false;
@@ -52,6 +68,12 @@ FloatingWindow {
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;
minWidthInput.text = "";
maxWidthInput.text = "";
minHeightInput.text = "";
@@ -84,18 +106,39 @@ FloatingWindow {
Qt.callLater(() => nameInput.forceActiveFocus());
}
function showEdit(rule) {
if (!rule) {
show();
return;
}
editingRule = rule;
resetForm();
function triFromBool(v) {
if (v === true)
return 1;
if (v === false)
return 2;
return 0;
}
function populateForm(rule) {
nameInput.text = rule.name || "";
const match = rule.matchCriteria || {};
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;
@@ -131,6 +174,15 @@ FloatingWindow {
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;
minWidthInput.text = actions.minWidth !== undefined ? String(actions.minWidth) : "";
maxWidthInput.text = actions.maxWidth !== undefined ? String(actions.maxWidth) : "";
minHeightInput.text = actions.minHeight !== undefined ? String(actions.minHeight) : "";
@@ -150,7 +202,28 @@ FloatingWindow {
moveInput.text = actions.move || "";
monitorInput.text = actions.monitor || "";
hyprWorkspaceInput.text = actions.workspace || "";
}
function showEdit(rule) {
if (!rule) {
show();
return;
}
editingRule = rule;
resetForm();
populateForm(rule);
visible = true;
Qt.callLater(() => nameInput.forceActiveFocus());
}
function showCopy(rule) {
if (!rule) {
show();
return;
}
editingRule = null;
resetForm();
populateForm(rule);
visible = true;
Qt.callLater(() => nameInput.forceActiveFocus());
}
@@ -161,6 +234,13 @@ FloatingWindow {
targetWindow = null;
}
function applyCond(obj, key, triState) {
if (triState === 1)
obj[key] = true;
else if (triState === 2)
obj[key] = false;
}
function submitAndClose() {
const matchCriteria = {};
if (appIdInput.text.trim())
@@ -168,6 +248,38 @@ FloatingWindow {
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)
@@ -206,6 +318,14 @@ FloatingWindow {
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 minW = parseInt(minWidthInput.text);
const maxW = parseInt(maxWidthInput.text);
@@ -260,6 +380,8 @@ FloatingWindow {
actions: actions,
enabled: true
};
if (isNiri && extraMatchModel.count > 0)
ruleData.matches = matches;
submitting = true;
@@ -369,6 +491,61 @@ FloatingWindow {
border.width: hasFocus ? 2 : 1
}
// Tri-state toggle: 0 = unset (Inherit/Any), 1 = true, 2 = false
component MatchCond: Rectangle {
id: mc
property string label: ""
property int triState: 0
property string unsetLabel: I18n.tr("Any")
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.cornerRadius
color: Theme.surfaceHover
border.width: 1
border.color: mc.triState === 0 ? Theme.outlineStrong : mc.stateColor[mc.triState]
opacity: mc.readOnly ? 0.4 : 1
Row {
id: condRow
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: mc.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: stateBadge.implicitWidth + Theme.spacingS * 2
height: 18
radius: 9
color: Theme.withAlpha(mc.stateColor[mc.triState], 0.15)
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: stateBadge
anchors.centerIn: parent
text: mc.stateText[mc.triState]
font.pixelSize: Theme.fontSizeSmall - 2
color: mc.stateColor[mc.triState]
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: root.visible && !mc.readOnly
onClicked: mc.triState = (mc.triState + 1) % 3
}
}
FocusScope {
anchors.fill: parent
focus: true
@@ -514,6 +691,176 @@ FloatingWindow {
}
}
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
InputField {
width: (parent.width - removeMatchBtn.width - Theme.spacingS * 2) / 2
hasFocus: extraAppId.activeFocus
DankTextField {
id: extraAppId
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: root.isNiri ? I18n.tr("App ID regex") : I18n.tr("Class regex")
backgroundColor: "transparent"
enabled: root.visible
text: rowAppId
onTextEdited: extraMatchModel.setProperty(index, "rowAppId", text)
}
}
InputField {
width: (parent.width - removeMatchBtn.width - Theme.spacingS * 2) / 2
hasFocus: extraTitle.activeFocus
DankTextField {
id: extraTitle
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: I18n.tr("Title regex (optional)")
backgroundColor: "transparent"
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
tooltipText: I18n.tr("Remove match")
tooltipSide: "left"
onClicked: extraMatchModel.remove(index)
}
}
}
Item {
width: parent.width
height: root.inputFieldHeight
visible: root.isNiri
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "add"
size: 18
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Add match")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: extraMatchModel.append({
"rowAppId": "",
"rowTitle": ""
})
}
}
SectionHeader {
title: I18n.tr("Match Conditions")
}
StyledText {
width: parent.width
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
MatchCond {
id: condFloating
label: I18n.tr("Floating")
}
MatchCond {
id: condActive
label: I18n.tr("Active")
visible: isNiri
}
MatchCond {
id: condFocused
label: I18n.tr("Focused")
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")
visible: isNiri
}
MatchCond {
id: condAtStartup
label: I18n.tr("At Startup")
visible: isNiri
}
MatchCond {
id: condXwayland
label: I18n.tr("XWayland")
visible: isHyprland
}
MatchCond {
id: condFullscreen
label: I18n.tr("Fullscreen")
visible: isHyprland
}
MatchCond {
id: condPinned
label: I18n.tr("Pinned")
visible: isHyprland
}
MatchCond {
id: condInitialised
label: I18n.tr("Initialised")
visible: isHyprland
}
}
SectionHeader {
title: I18n.tr("Window Opening")
}
@@ -682,6 +1029,7 @@ FloatingWindow {
DankSlider {
id: opacitySlider
wheelEnabled: false
width: parent.width - 100
minimum: 10
maximum: 100
@@ -710,7 +1058,7 @@ FloatingWindow {
}
CheckboxRow {
id: drawBorderBgToggle
label: I18n.tr("Border with BG")
label: I18n.tr("Border with Background")
}
}
@@ -777,6 +1125,7 @@ FloatingWindow {
DankSlider {
id: scrollFactorSlider
wheelEnabled: false
width: parent.width - 120
minimum: 10
maximum: 200
@@ -798,6 +1147,7 @@ FloatingWindow {
DankSlider {
id: cornerRadiusSlider
wheelEnabled: false
width: parent.width - 130
minimum: 0
maximum: 24
@@ -807,6 +1157,88 @@ FloatingWindow {
}
}
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")
unsetLabel: I18n.tr("Inherit")
onTriStateChanged: {
if (triState === 2)
xrayCond.triState = 0;
}
}
MatchCond {
id: xrayCond
label: I18n.tr("X-Ray")
unsetLabel: I18n.tr("Inherit")
readOnly: blurCond.triState === 2
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: isNiri
CheckboxRow {
id: noiseEnabled
label: I18n.tr("Noise")
anchors.verticalCenter: parent.verticalCenter
}
DankSlider {
id: noiseSlider
wheelEnabled: false
width: parent.width - 130
minimum: 0
maximum: 100
value: 5
enabled: noiseEnabled.checked
opacity: enabled ? 1 : 0.4
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: isNiri
CheckboxRow {
id: saturationEnabled
label: I18n.tr("Saturation")
anchors.verticalCenter: parent.verticalCenter
}
DankSlider {
id: saturationSlider
wheelEnabled: false
width: parent.width - 130
minimum: 0
maximum: 200
value: 100
enabled: saturationEnabled.checked
opacity: enabled ? 1 : 0.4
}
}
SectionHeader {
title: I18n.tr("Size Constraints")
}
+408 -47
View File
@@ -27,7 +27,107 @@ Item {
property bool checkingInclude: false
property bool fixingInclude: false
property var windowRules: []
property var externalRules: []
property var activeWindows: getActiveWindows()
property string expandedExternalId: ""
readonly property var matchLabels: ({
"appId": I18n.tr("App ID"),
"title": I18n.tr("Title"),
"isFloating": I18n.tr("Is Floating"),
"isActive": I18n.tr("Is Active"),
"isFocused": I18n.tr("Is Focused"),
"isActiveInColumn": I18n.tr("Active In Column"),
"isWindowCastTarget": I18n.tr("Cast Target"),
"isUrgent": I18n.tr("Is Urgent"),
"atStartup": I18n.tr("At Startup"),
"xwayland": I18n.tr("XWayland"),
"fullscreen": I18n.tr("Fullscreen"),
"pinned": I18n.tr("Pinned"),
"initialised": I18n.tr("Initialised")
})
function matchesOf(rule) {
const m = rule.matches;
if (m && m.length > 0)
return m;
return [rule.matchCriteria || {}];
}
function formatCriteria(obj, labels) {
let out = [];
const keys = Object.keys(obj || {});
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const v = obj[k];
if (v === undefined || v === null || v === "")
continue;
const label = labels[k] || k;
if (typeof v === "boolean")
out.push(label + ": " + (v ? I18n.tr("yes") : I18n.tr("no")));
else
out.push(label + ": " + v);
}
return out;
}
function matchSummary(rule) {
const matches = matchesOf(rule);
const first = matches[0] || {};
const label = first.appId || first.title || I18n.tr("Any window");
if (matches.length > 1)
return I18n.tr("%1 (+%2 more)").arg(label).arg(matches.length - 1);
return label;
}
readonly property var actionLabels: ({
"opacity": I18n.tr("Opacity"),
"openFloating": I18n.tr("Float"),
"openMaximized": I18n.tr("Maximize"),
"openMaximizedToEdges": I18n.tr("Max Edges"),
"openFullscreen": I18n.tr("Fullscreen"),
"openFocused": I18n.tr("Focus"),
"openOnOutput": I18n.tr("Output"),
"openOnWorkspace": I18n.tr("Workspace"),
"defaultColumnWidth": I18n.tr("Width"),
"defaultWindowHeight": I18n.tr("Height"),
"variableRefreshRate": I18n.tr("VRR"),
"blockOutFrom": I18n.tr("Block Out"),
"defaultColumnDisplay": I18n.tr("Display"),
"scrollFactor": I18n.tr("Scroll"),
"cornerRadius": I18n.tr("Radius"),
"clipToGeometry": I18n.tr("Clip"),
"tiledState": I18n.tr("Tiled"),
"minWidth": I18n.tr("Min W"),
"maxWidth": I18n.tr("Max W"),
"minHeight": I18n.tr("Min H"),
"maxHeight": I18n.tr("Max H"),
"tile": I18n.tr("Tile"),
"nofocus": I18n.tr("No Focus"),
"noborder": I18n.tr("No Border"),
"noshadow": I18n.tr("No Shadow"),
"nodim": I18n.tr("No Dim"),
"noblur": I18n.tr("No Blur"),
"noanim": I18n.tr("No Anim"),
"norounding": I18n.tr("No Round"),
"pin": I18n.tr("Pin"),
"opaque": I18n.tr("Opaque"),
"size": I18n.tr("Size"),
"move": I18n.tr("Move"),
"monitor": I18n.tr("Monitor"),
"workspace": I18n.tr("Workspace"),
"drawBorderWithBackground": I18n.tr("Border w/ Bg"),
"backgroundBlur": I18n.tr("Blur"),
"backgroundXray": I18n.tr("X-Ray"),
"backgroundNoise": I18n.tr("Noise"),
"backgroundSaturation": I18n.tr("Saturation"),
"borderColor": I18n.tr("Border Color"),
"focusRingColor": I18n.tr("Focus Ring Color"),
"focusRingOff": I18n.tr("Focus Ring Off"),
"borderOff": I18n.tr("Border Off"),
"forcergbx": I18n.tr("Force RGBX"),
"idleinhibit": I18n.tr("Idle Inhibit")
})
signal rulesChanged
@@ -72,18 +172,21 @@ Item {
const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland") {
windowRules = [];
externalRules = [];
return;
}
Proc.runCommand("load-windowrules", ["dms", "config", "windowrules", "list", compositor], (output, exitCode) => {
if (exitCode !== 0) {
windowRules = [];
externalRules = [];
return;
}
try {
const result = JSON.parse(output.trim());
const allRules = result.rules || [];
windowRules = allRules.filter(r => (r.source || "").includes("dms/windowrules"));
externalRules = allRules.filter(r => !(r.source || "").includes("dms/windowrules"));
if (result.dmsStatus) {
windowRulesIncludeStatus = {
"exists": result.dmsStatus.exists,
@@ -94,6 +197,7 @@ Item {
}
} catch (e) {
windowRules = [];
externalRules = [];
}
});
}
@@ -232,6 +336,20 @@ Item {
}
}
function copyRuleToDms(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.showCopy(rule);
}
}
function showHyprlandReadOnlyWarning() {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing window rules in Settings."), "dms setup", "hyprland-migration");
}
@@ -311,6 +429,8 @@ Item {
iconColor: Theme.primary
enabled: !root.readOnly
opacity: enabled ? 1 : 0.5
tooltipText: I18n.tr("Add Window Rule")
tooltipSide: "left"
onClicked: root.openRuleModal()
}
}
@@ -575,47 +695,11 @@ Item {
Repeater {
model: {
const a = ruleDelegateItem.liveRuleData.actions || {};
const labels = {
"opacity": I18n.tr("Opacity"),
"openFloating": I18n.tr("Float"),
"openMaximized": I18n.tr("Maximize"),
"openMaximizedToEdges": I18n.tr("Max Edges"),
"openFullscreen": I18n.tr("Fullscreen"),
"openFocused": I18n.tr("Focus"),
"openOnOutput": I18n.tr("Output"),
"openOnWorkspace": I18n.tr("Workspace"),
"defaultColumnWidth": I18n.tr("Width"),
"defaultWindowHeight": I18n.tr("Height"),
"variableRefreshRate": I18n.tr("VRR"),
"blockOutFrom": I18n.tr("Block Out"),
"defaultColumnDisplay": I18n.tr("Display"),
"scrollFactor": I18n.tr("Scroll"),
"cornerRadius": I18n.tr("Radius"),
"clipToGeometry": I18n.tr("Clip"),
"tiledState": I18n.tr("Tiled"),
"minWidth": I18n.tr("Min W"),
"maxWidth": I18n.tr("Max W"),
"minHeight": I18n.tr("Min H"),
"maxHeight": I18n.tr("Max H"),
"tile": I18n.tr("Tile"),
"nofocus": I18n.tr("No Focus"),
"noborder": I18n.tr("No Border"),
"noshadow": I18n.tr("No Shadow"),
"nodim": I18n.tr("No Dim"),
"noblur": I18n.tr("No Blur"),
"noanim": I18n.tr("No Anim"),
"norounding": I18n.tr("No Round"),
"pin": I18n.tr("Pin"),
"opaque": I18n.tr("Opaque"),
"size": I18n.tr("Size"),
"move": I18n.tr("Move"),
"monitor": I18n.tr("Monitor"),
"workspace": I18n.tr("Workspace")
};
const labels = root.actionLabels;
return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => {
const val = a[k];
if (typeof val === "boolean")
return labels[k] || k;
return val ? (labels[k] || k) : (labels[k] || k) + ": " + I18n.tr("off");
return (labels[k] || k) + ": " + val;
});
}
@@ -651,26 +735,26 @@ Item {
iconColor: Theme.surfaceVariantText
enabled: !root.readOnly
opacity: enabled ? 1 : 0.5
tooltipText: I18n.tr("Edit Rule")
tooltipSide: "top"
onClicked: root.editRule(ruleDelegateItem.liveRuleData)
}
DankActionButton {
id: deleteBtn
property bool hovered: false
buttonSize: 28
iconName: "delete"
iconSize: 16
backgroundColor: "transparent"
iconColor: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText
iconColor: hovered ? Theme.error : Theme.surfaceVariantText
enabled: !root.readOnly
opacity: enabled ? 1 : 0.5
MouseArea {
id: deleteArea
anchors.fill: parent
hoverEnabled: !root.readOnly
cursorShape: root.readOnly ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: root.removeRule(ruleDelegateItem.ruleIdRef)
}
tooltipText: I18n.tr("Delete Rule")
tooltipSide: "top"
onEntered: hovered = true
onExited: hovered = false
onClicked: root.removeRule(ruleDelegateItem.ruleIdRef)
}
}
}
@@ -729,6 +813,283 @@ Item {
}
}
}
StyledRect {
width: Math.min(650, parent.width - Theme.spacingL * 2)
height: externalSection.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
visible: root.externalRules && root.externalRules.length > 0
Column {
id: externalSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
RowLayout {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "description"
size: Theme.iconSize
color: Theme.primary
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("User Window Rules (%1)").arg(root.externalRules?.length ?? 0)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
Layout.fillWidth: true
}
StyledText {
text: I18n.tr("Rules found in your compositor config. These are read-only here, use Convert to DMS to make an editable copy.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: ScriptModel {
objectProp: "id"
values: root.externalRules || []
}
delegate: Rectangle {
id: externalCard
required property var modelData
readonly property string displayName: {
const name = externalCard.modelData.name || "";
if (name)
return name;
return root.matchSummary(externalCard.modelData);
}
readonly property string sourceFile: (externalCard.modelData.source || "").split("/").pop()
readonly property bool expanded: root.expandedExternalId === externalCard.modelData.id
width: parent.width
height: externalContent.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, 0.4)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.expandedExternalId = externalCard.expanded ? "" : externalCard.modelData.id
}
Column {
id: externalContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
width: parent.width
spacing: Theme.spacingM
ColumnLayout {
Layout.fillWidth: true
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingS
StyledText {
text: externalCard.displayName
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
Layout.fillWidth: true
}
Rectangle {
visible: externalCard.sourceFile.length > 0
width: sourceText.implicitWidth + Theme.spacingS * 2
height: 20
radius: 10
color: Theme.withAlpha(Theme.surfaceVariantText, 0.15)
Layout.alignment: Qt.AlignVCenter
StyledText {
id: sourceText
anchors.centerIn: parent
text: externalCard.sourceFile
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.surfaceVariantText
}
}
}
StyledText {
text: {
const m = externalCard.modelData.matchCriteria || {};
let parts = [];
if (m.appId)
parts.push(m.appId);
if (m.title)
parts.push("title: " + m.title);
const base = parts.length > 0 ? parts.join(" · ") : I18n.tr("No match criteria");
const count = root.matchesOf(externalCard.modelData).length;
return count > 1 ? I18n.tr("%1 (+%2 more)").arg(base).arg(count - 1) : base;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
Layout.fillWidth: true
}
Flow {
Layout.fillWidth: true
Layout.topMargin: 4
spacing: Theme.spacingXS
visible: {
const a = externalCard.modelData.actions || {};
return Object.keys(a).some(k => a[k] !== undefined && a[k] !== null && a[k] !== "");
}
Repeater {
model: {
const a = externalCard.modelData.actions || {};
const labels = root.actionLabels;
return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => {
const val = a[k];
if (typeof val === "boolean")
return val ? (labels[k] || k) : (labels[k] || k) + ": " + I18n.tr("off");
return (labels[k] || k) + ": " + val;
});
}
delegate: Rectangle {
required property string modelData
width: extChipText.implicitWidth + Theme.spacingS * 2
height: 20
radius: 10
color: Theme.withAlpha(Theme.primary, 0.15)
StyledText {
id: extChipText
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.primary
}
}
}
}
}
DankIcon {
name: externalCard.expanded ? "expand_less" : "expand_more"
size: 20
color: Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter
}
DankActionButton {
buttonSize: 28
iconName: "content_copy"
iconSize: 16
backgroundColor: "transparent"
iconColor: Theme.surfaceVariantText
enabled: !root.readOnly
opacity: enabled ? 1 : 0.5
Layout.alignment: Qt.AlignVCenter
tooltipText: I18n.tr("Convert to DMS")
tooltipSide: "left"
onClicked: root.copyRuleToDms(externalCard.modelData)
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: externalCard.expanded
Rectangle {
width: parent.width
height: 1
color: Theme.withAlpha(Theme.outline, 0.5)
}
StyledText {
text: I18n.tr("Match (%1)").arg(root.matchesOf(externalCard.modelData).length)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
Repeater {
model: root.matchesOf(externalCard.modelData)
delegate: StyledText {
required property var modelData
width: parent.width
text: {
const c = root.formatCriteria(modelData, root.matchLabels);
return "• " + (c.length > 0 ? c.join(" · ") : I18n.tr("Any window"));
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
StyledText {
text: I18n.tr("Actions")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingXS
}
StyledText {
width: parent.width
text: {
const a = root.formatCriteria(externalCard.modelData.actions, root.actionLabels);
return a.length > 0 ? a.join(" · ") : I18n.tr("None");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StyledText {
width: parent.width
text: I18n.tr("Source: %1").arg(externalCard.modelData.source || "")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
topPadding: Theme.spacingXS
}
}
}
}
}
}
}
}
}
}
}