mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 23:42:51 -05:00
- settings UI for creating, editing, deleting window ruels - IPC to create a window rule for the currently focused toplevel fixes #1292
691 lines
33 KiB
QML
691 lines
33 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtCore
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
LayoutMirroring.enabled: I18n.isRtl
|
|
LayoutMirroring.childrenInherit: true
|
|
|
|
property var parentModal: null
|
|
property var windowRulesIncludeStatus: ({
|
|
"exists": false,
|
|
"included": false
|
|
})
|
|
property bool checkingInclude: false
|
|
property bool fixingInclude: false
|
|
property var windowRules: []
|
|
property var activeWindows: getActiveWindows()
|
|
|
|
signal rulesChanged
|
|
|
|
function getActiveWindows() {
|
|
const toplevels = ToplevelManager.toplevels?.values || [];
|
|
return toplevels.map(t => ({
|
|
appId: t.appId || "",
|
|
title: t.title || ""
|
|
}));
|
|
}
|
|
|
|
Connections {
|
|
target: ToplevelManager.toplevels
|
|
function onValuesChanged() {
|
|
root.activeWindows = root.getActiveWindows();
|
|
}
|
|
}
|
|
|
|
function getWindowRulesConfigPaths() {
|
|
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
|
|
switch (CompositorService.compositor) {
|
|
case "niri":
|
|
return {
|
|
"configFile": configDir + "/niri/config.kdl",
|
|
"rulesFile": configDir + "/niri/dms/windowrules.kdl",
|
|
"grepPattern": 'include.*"dms/windowrules.kdl"',
|
|
"includeLine": 'include "dms/windowrules.kdl"'
|
|
};
|
|
case "hyprland":
|
|
return {
|
|
"configFile": configDir + "/hypr/hyprland.conf",
|
|
"rulesFile": configDir + "/hypr/dms/windowrules.conf",
|
|
"grepPattern": 'source.*dms/windowrules.conf',
|
|
"includeLine": "source = ./dms/windowrules.conf"
|
|
};
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function loadWindowRules() {
|
|
const compositor = CompositorService.compositor;
|
|
if (compositor !== "niri" && compositor !== "hyprland") {
|
|
windowRules = [];
|
|
return;
|
|
}
|
|
|
|
Proc.runCommand("load-windowrules", ["dms", "config", "windowrules", "list", compositor], (output, exitCode) => {
|
|
if (exitCode !== 0) {
|
|
windowRules = [];
|
|
return;
|
|
}
|
|
try {
|
|
const result = JSON.parse(output.trim());
|
|
const allRules = result.rules || [];
|
|
windowRules = allRules.filter(r => (r.source || "").includes("dms/windowrules"));
|
|
if (result.dmsStatus) {
|
|
windowRulesIncludeStatus = {
|
|
"exists": result.dmsStatus.exists,
|
|
"included": result.dmsStatus.included
|
|
};
|
|
}
|
|
} catch (e) {
|
|
windowRules = [];
|
|
}
|
|
});
|
|
}
|
|
|
|
function removeRule(ruleId) {
|
|
const compositor = CompositorService.compositor;
|
|
if (compositor !== "niri" && compositor !== "hyprland")
|
|
return;
|
|
|
|
Proc.runCommand("remove-windowrule", ["dms", "config", "windowrules", "remove", compositor, ruleId], (output, exitCode) => {
|
|
if (exitCode === 0) {
|
|
loadWindowRules();
|
|
rulesChanged();
|
|
}
|
|
});
|
|
}
|
|
|
|
function reorderRules(fromIndex, toIndex) {
|
|
if (fromIndex === toIndex)
|
|
return;
|
|
|
|
const compositor = CompositorService.compositor;
|
|
if (compositor !== "niri" && compositor !== "hyprland")
|
|
return;
|
|
|
|
let ids = windowRules.map(r => r.id);
|
|
const [moved] = ids.splice(fromIndex, 1);
|
|
ids.splice(toIndex, 0, moved);
|
|
|
|
Proc.runCommand("reorder-windowrules", ["dms", "config", "windowrules", "reorder", compositor, JSON.stringify(ids)], (output, exitCode) => {
|
|
if (exitCode === 0) {
|
|
loadWindowRules();
|
|
rulesChanged();
|
|
}
|
|
});
|
|
}
|
|
|
|
function checkWindowRulesIncludeStatus() {
|
|
const compositor = CompositorService.compositor;
|
|
if (compositor !== "niri" && compositor !== "hyprland") {
|
|
windowRulesIncludeStatus = {
|
|
"exists": false,
|
|
"included": false
|
|
};
|
|
return;
|
|
}
|
|
|
|
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.conf";
|
|
checkingInclude = true;
|
|
Proc.runCommand("check-windowrules-include", ["dms", "config", "resolve-include", compositor, filename], (output, exitCode) => {
|
|
checkingInclude = false;
|
|
if (exitCode !== 0) {
|
|
windowRulesIncludeStatus = {
|
|
"exists": false,
|
|
"included": false
|
|
};
|
|
return;
|
|
}
|
|
try {
|
|
windowRulesIncludeStatus = JSON.parse(output.trim());
|
|
} catch (e) {
|
|
windowRulesIncludeStatus = {
|
|
"exists": false,
|
|
"included": false
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
function fixWindowRulesInclude() {
|
|
const paths = getWindowRulesConfigPaths();
|
|
if (!paths)
|
|
return;
|
|
fixingInclude = true;
|
|
const rulesDir = paths.rulesFile.substring(0, paths.rulesFile.lastIndexOf("/"));
|
|
const unixTime = Math.floor(Date.now() / 1000);
|
|
const backupFile = paths.configFile + ".backup" + unixTime;
|
|
Proc.runCommand("fix-windowrules-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${rulesDir}" && ` + `touch "${paths.rulesFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
|
|
fixingInclude = false;
|
|
if (exitCode !== 0)
|
|
return;
|
|
checkWindowRulesIncludeStatus();
|
|
loadWindowRules();
|
|
});
|
|
}
|
|
|
|
function openRuleModal(window) {
|
|
if (!PopoutService.windowRuleModalLoader)
|
|
return;
|
|
PopoutService.windowRuleModalLoader.active = true;
|
|
if (PopoutService.windowRuleModalLoader.item) {
|
|
PopoutService.windowRuleModalLoader.item.onRuleSubmitted.connect(loadWindowRules);
|
|
PopoutService.windowRuleModalLoader.item.show(window || null);
|
|
}
|
|
}
|
|
|
|
function editRule(rule) {
|
|
if (!PopoutService.windowRuleModalLoader)
|
|
return;
|
|
PopoutService.windowRuleModalLoader.active = true;
|
|
if (PopoutService.windowRuleModalLoader.item) {
|
|
PopoutService.windowRuleModalLoader.item.onRuleSubmitted.connect(loadWindowRules);
|
|
PopoutService.windowRuleModalLoader.item.showEdit(rule);
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (CompositorService.isNiri || CompositorService.isHyprland) {
|
|
checkWindowRulesIncludeStatus();
|
|
loadWindowRules();
|
|
}
|
|
}
|
|
|
|
DankFlickable {
|
|
id: flickable
|
|
anchors.fill: parent
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: contentColumn.implicitHeight
|
|
|
|
Column {
|
|
id: contentColumn
|
|
width: flickable.width
|
|
spacing: Theme.spacingL
|
|
topPadding: Theme.spacingXL
|
|
bottomPadding: Theme.spacingXL
|
|
|
|
StyledRect {
|
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
|
height: headerSection.implicitHeight + Theme.spacingL * 2
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
radius: Theme.cornerRadius
|
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
|
|
Column {
|
|
id: headerSection
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
RowLayout {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "select_window"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Window Rules")
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.conf")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
wrapMode: Text.WordWrap
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
Layout.preferredWidth: 40
|
|
Layout.preferredHeight: 40
|
|
circular: false
|
|
iconName: "add"
|
|
iconSize: Theme.iconSize
|
|
iconColor: Theme.primary
|
|
onClicked: root.openRuleModal()
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
visible: root.activeWindows.length > 0
|
|
|
|
StyledText {
|
|
text: I18n.tr("Create rule for:")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
DankDropdown {
|
|
id: windowSelector
|
|
Layout.fillWidth: true
|
|
dropdownWidth: 400
|
|
compactMode: true
|
|
emptyText: I18n.tr("Select a window...")
|
|
options: root.activeWindows.map(w => {
|
|
const label = w.appId + (w.title ? " - " + w.title : "");
|
|
return label.length > 60 ? label.substring(0, 57) + "..." : label;
|
|
})
|
|
onValueChanged: value => {
|
|
if (!value)
|
|
return;
|
|
const index = options.indexOf(value);
|
|
if (index < 0 || index >= root.activeWindows.length)
|
|
return;
|
|
const window = root.activeWindows[index];
|
|
root.openRuleModal(window);
|
|
currentValue = "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
id: warningBox
|
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
|
height: warningSection.implicitHeight + Theme.spacingL * 2
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
radius: Theme.cornerRadius
|
|
|
|
readonly property bool showError: root.windowRulesIncludeStatus.exists && !root.windowRulesIncludeStatus.included
|
|
readonly property bool showSetup: !root.windowRulesIncludeStatus.exists && !root.windowRulesIncludeStatus.included
|
|
|
|
color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.15) : "transparent"
|
|
border.color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.3) : "transparent"
|
|
border.width: 1
|
|
visible: (showError || showSetup) && !root.checkingInclude && (CompositorService.isNiri || CompositorService.isHyprland)
|
|
|
|
Row {
|
|
id: warningSection
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "warning"
|
|
size: Theme.iconSize
|
|
color: Theme.warning
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM
|
|
spacing: Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
StyledText {
|
|
text: warningBox.showSetup ? I18n.tr("Window Rules Not Configured") : I18n.tr("Window Rules Include Missing")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.warning
|
|
}
|
|
|
|
StyledText {
|
|
readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.conf"
|
|
text: warningBox.showSetup ? I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(rulesFile) : I18n.tr("%1 exists but is not included. Window rules won't apply.").arg(rulesFile)
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
wrapMode: Text.WordWrap
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
DankButton {
|
|
id: fixButton
|
|
visible: warningBox.showError || warningBox.showSetup
|
|
text: root.fixingInclude ? I18n.tr("Fixing...") : (warningBox.showSetup ? I18n.tr("Setup") : I18n.tr("Fix Now"))
|
|
backgroundColor: Theme.warning
|
|
textColor: Theme.background
|
|
enabled: !root.fixingInclude
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: root.fixWindowRulesInclude()
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
|
height: rulesSection.implicitHeight + Theme.spacingL * 2
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
radius: Theme.cornerRadius
|
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
|
|
Column {
|
|
id: rulesSection
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
RowLayout {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "list"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Rules (%1)").arg(root.windowRules?.length ?? 0)
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
visible: !root.windowRules || root.windowRules.length === 0
|
|
|
|
Item {
|
|
width: 1
|
|
height: Theme.spacingM
|
|
}
|
|
|
|
DankIcon {
|
|
name: "select_window"
|
|
size: 40
|
|
color: Theme.surfaceVariantText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
opacity: 0.5
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("No window rules configured")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Click + to add a rule for the focused window")
|
|
font.pixelSize: Theme.fontSizeSmall - 1
|
|
color: Theme.surfaceVariantText
|
|
opacity: 0.7
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
Item {
|
|
width: 1
|
|
height: Theme.spacingM
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: rulesListColumn
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
visible: root.windowRules && root.windowRules.length > 0
|
|
|
|
Repeater {
|
|
model: ScriptModel {
|
|
objectProp: "id"
|
|
values: root.windowRules || []
|
|
}
|
|
|
|
delegate: Item {
|
|
id: ruleDelegateItem
|
|
required property var modelData
|
|
required property int index
|
|
|
|
property bool held: ruleDragArea.pressed
|
|
property real originalY: y
|
|
|
|
readonly property string ruleIdRef: modelData.id
|
|
readonly property var liveRuleData: {
|
|
const rules = root.windowRules || [];
|
|
return rules.find(r => r.id === ruleIdRef) ?? modelData;
|
|
}
|
|
readonly property string displayName: {
|
|
const name = liveRuleData.name || "";
|
|
if (name)
|
|
return name;
|
|
const m = liveRuleData.matchCriteria || {};
|
|
return m.appId || m.title || I18n.tr("Unnamed Rule");
|
|
}
|
|
|
|
width: rulesListColumn.width
|
|
height: ruleCard.height
|
|
z: held ? 2 : 1
|
|
|
|
Rectangle {
|
|
id: ruleCard
|
|
width: parent.width
|
|
height: ruleContent.implicitHeight + Theme.spacingM * 2
|
|
radius: Theme.cornerRadius
|
|
color: ruleDelegateItem.liveRuleData.enabled !== false ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.4)
|
|
|
|
RowLayout {
|
|
id: ruleContent
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingM
|
|
anchors.leftMargin: 28
|
|
spacing: Theme.spacingM
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
|
|
StyledText {
|
|
text: ruleDelegateItem.displayName
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: ruleDelegateItem.liveRuleData.enabled !== false ? Theme.surfaceText : Theme.surfaceVariantText
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
StyledText {
|
|
text: {
|
|
const m = ruleDelegateItem.liveRuleData.matchCriteria || {};
|
|
let parts = [];
|
|
if (m.appId)
|
|
parts.push(m.appId);
|
|
if (m.title)
|
|
parts.push("title: " + m.title);
|
|
return parts.length > 0 ? parts.join(" · ") : I18n.tr("No match criteria");
|
|
}
|
|
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 = ruleDelegateItem.liveRuleData.actions || {};
|
|
return Object.keys(a).some(k => a[k] !== undefined && a[k] !== null && a[k] !== "");
|
|
}
|
|
|
|
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")
|
|
};
|
|
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 (labels[k] || k) + ": " + val;
|
|
});
|
|
}
|
|
|
|
delegate: Rectangle {
|
|
required property string modelData
|
|
width: chipText.implicitWidth + Theme.spacingS * 2
|
|
height: 20
|
|
radius: 10
|
|
color: Theme.withAlpha(Theme.primary, 0.15)
|
|
|
|
StyledText {
|
|
id: chipText
|
|
anchors.centerIn: parent
|
|
text: modelData
|
|
font.pixelSize: Theme.fontSizeSmall - 2
|
|
color: Theme.primary
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignVCenter
|
|
spacing: 2
|
|
|
|
DankActionButton {
|
|
buttonSize: 28
|
|
iconName: "edit"
|
|
iconSize: 16
|
|
backgroundColor: "transparent"
|
|
iconColor: Theme.surfaceVariantText
|
|
onClicked: root.editRule(ruleDelegateItem.liveRuleData)
|
|
}
|
|
|
|
DankActionButton {
|
|
id: deleteBtn
|
|
buttonSize: 28
|
|
iconName: "delete"
|
|
iconSize: 16
|
|
backgroundColor: "transparent"
|
|
iconColor: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText
|
|
|
|
MouseArea {
|
|
id: deleteArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.removeRule(ruleDelegateItem.ruleIdRef)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: ruleDragArea
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
width: 40
|
|
height: ruleCard.height
|
|
hoverEnabled: true
|
|
cursorShape: Qt.SizeVerCursor
|
|
drag.target: ruleDelegateItem.held ? ruleDelegateItem : undefined
|
|
drag.axis: Drag.YAxis
|
|
preventStealing: true
|
|
|
|
onPressed: {
|
|
ruleDelegateItem.z = 2;
|
|
ruleDelegateItem.originalY = ruleDelegateItem.y;
|
|
}
|
|
onReleased: {
|
|
ruleDelegateItem.z = 1;
|
|
if (!drag.active) {
|
|
ruleDelegateItem.y = ruleDelegateItem.originalY;
|
|
return;
|
|
}
|
|
const spacing = Theme.spacingXS;
|
|
const itemH = ruleDelegateItem.height + spacing;
|
|
var newIndex = Math.round(ruleDelegateItem.y / itemH);
|
|
newIndex = Math.max(0, Math.min(newIndex, (root.windowRules?.length ?? 1) - 1));
|
|
if (newIndex !== ruleDelegateItem.index)
|
|
root.reorderRules(ruleDelegateItem.index, newIndex);
|
|
ruleDelegateItem.y = ruleDelegateItem.originalY;
|
|
}
|
|
}
|
|
|
|
DankIcon {
|
|
x: Theme.spacingM - 2
|
|
y: (ruleCard.height / 2) - (size / 2)
|
|
name: "drag_indicator"
|
|
size: 18
|
|
color: Theme.outline
|
|
opacity: ruleDragArea.containsMouse || ruleDragArea.pressed ? 1 : 0.5
|
|
}
|
|
|
|
Behavior on y {
|
|
enabled: !ruleDragArea.pressed && !ruleDragArea.drag.active
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Theme.standardEasing
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|