From 4684971b2ad65a04ca73020c99da3dceb9d3a9fb Mon Sep 17 00:00:00 2001
From: Anton Kharuzhy <publicantroids@gmail.com>
Date: Wed, 10 Jul 2024 17:50:12 +0200
Subject: [PATCH] #44: sortable replacements, replacements listeners decoupled
 to avoid dead-locking

---
 .../contents/ui/config/TitleReplacements.qml  | 177 ++++++++++++++----
 package/contents/ui/main.qml                  |  72 ++++---
 package/contents/ui/utils.js                  |  15 +-
 3 files changed, 197 insertions(+), 67 deletions(-)

diff --git a/package/contents/ui/config/TitleReplacements.qml b/package/contents/ui/config/TitleReplacements.qml
index d1df1bc..777dcb0 100644
--- a/package/contents/ui/config/TitleReplacements.qml
+++ b/package/contents/ui/config/TitleReplacements.qml
@@ -26,12 +26,85 @@ KCM.SimpleKCM {
         Regex
     }
 
+    ListModel {
+        id: replacementsModel
+
+        function updateModelFromConfig() {
+            clear();
+            for (let i = 0; i < cfg_titleReplacementsPatterns.length; i++) {
+                append({
+                    "pattern": cfg_titleReplacementsPatterns[i],
+                    "template": cfg_titleReplacementsTemplates[i],
+                    "type": cfg_titleReplacementsTypes[i]
+                });
+            }
+        }
+
+        function updateConfigFromModel() {
+            Qt.callLater(_updateConfigFromModel);
+        }
+
+        function _updateConfigFromModel() {
+            const length = count;
+            cfg_titleReplacementsPatterns.length = length;
+            cfg_titleReplacementsTemplates.length = length;
+            cfg_titleReplacementsTypes.length = length;
+            for (let i = 0; i < length; i++) {
+                const rowValue = get(i);
+                cfg_titleReplacementsPatterns[i] = rowValue.pattern;
+                cfg_titleReplacementsTemplates[i] = rowValue.template;
+                cfg_titleReplacementsTypes[i] = rowValue.type;
+            }
+        }
+
+        function pushNewReplacement() {
+            append({
+                "pattern": "",
+                "template": "",
+                "type": TitleReplacements.Type.String
+            });
+            updateConfigFromModel();
+        }
+
+        function deleteReplacement(index) {
+            remove(index);
+            updateConfigFromModel();
+        }
+
+        function setType(index, type) {
+            set(index, {
+                "type": type
+            });
+            updateConfigFromModel();
+        }
+
+        function setPattern(index, pattern) {
+            set(index, {
+                "pattern": pattern
+            });
+            updateConfigFromModel();
+        }
+
+        function setTemplate(index, template) {
+            set(index, {
+                "template": template
+            });
+            updateConfigFromModel();
+        }
+
+        function moveReplacement(from, to) {
+            move(from, to, 1);
+            updateConfigFromModel();
+        }
+
+        Component.onCompleted: updateModelFromConfig()
+    }
+
     ColumnLayout {
         RowLayout {
-            visible: replacementsRepeater.count > 5
             Button {
                 text: i18n("Add Title Replacement")
-                onClicked: pushNewReplacement()
+                onClicked: replacementsModel.pushNewReplacement()
             }
             Label {
                 text: i18n("<a href=\"https://www.w3schools.com/jsref/jsref_obj_regexp.asp\">JavaScript RegExp Reference</a>")
@@ -44,34 +117,72 @@ KCM.SimpleKCM {
         Repeater {
             id: replacementsRepeater
 
-            function updateModel() {
-                model = cfg_titleReplacementsPatterns.length;
-            }
-
-            Component.onCompleted: updateModel()
-
+            model: replacementsModel
             delegate: RowLayout {
                 id: replacement
                 required property int index
+                required property var modelData
+
+                Drag.source: dragArea
+                Drag.active: dragArea.drag.active
+                Drag.hotSpot.y: height / 2
+
+                Kirigami.Icon {
+                    Layout.maximumWidth: Kirigami.Units.gridUnit
+
+                    source: "transform-move-vertical"
+                    MouseArea {
+                        id: dragArea
+                        anchors.fill: parent
+                        drag.axis: Drag.YAxis
+                        drag.target: replacement
+                        cursorShape: Qt.DragMoveCursor
+
+                        drag {
+                            onActiveChanged: function () {
+                                if (!drag.active) {
+                                    replacementsModel.updateModelFromConfig();
+                                }
+                            }
+                        }
+
+                        states: State {
+                            when: dragArea.drag.active
+
+                            PropertyChanges {
+                                target: replacement
+                                z: 1
+                            }
+                        }
+
+                        DropArea {
+                            anchors.fill: parent
+                            onEntered: drag => {
+                                replacementsModel.moveReplacement(drag.source.DelegateModel.itemsIndex, dragArea.DelegateModel.itemsIndex);
+                            }
+                        }
+                    }
+                }
 
                 ComboBox {
                     id: titleReplacementsType
+                    Layout.maximumWidth: Kirigami.Units.gridUnit * 5
 
                     model: [i18n("String"), i18n("RegExp")]
                     onActivated: function () {
-                        cfg_titleReplacementsTypes[replacement.index] = currentIndex;
+                        replacementsModel.setType(index, currentIndex);
                     }
-                    Component.onCompleted: currentIndex = cfg_titleReplacementsTypes[replacement.index]
+                    currentIndex: modelData.type
                 }
 
                 TextField {
                     id: titleReplacementsPattern
 
                     onTextEdited: function () {
-                        cfg_titleReplacementsPatterns[replacement.index] = text;
+                        replacementsModel.setPattern(index, text);
                     }
                     Layout.alignment: Qt.AlignLeft
-                    Component.onCompleted: text = cfg_titleReplacementsPatterns[replacement.index]
+                    text: modelData.pattern
                 }
 
                 Label {
@@ -82,24 +193,25 @@ KCM.SimpleKCM {
                     id: titleReplacementsTemplate
 
                     onTextEdited: function () {
-                        cfg_titleReplacementsTemplates[replacement.index] = text;
+                        replacementsModel.setTemplate(index, text);
                     }
                     Layout.alignment: Qt.AlignLeft
-                    Component.onCompleted: text = cfg_titleReplacementsTemplates[replacement.index]
+                    text: modelData.template
                 }
 
                 Button {
                     icon.name: "delete"
                     onClicked: function () {
-                        deleteReplacement(replacement.index);
+                        replacementsModel.deleteReplacement(replacement.index);
                     }
                 }
             }
         }
         RowLayout {
+            visible: replacementsRepeater.count > 5
             Button {
                 text: i18n("Add Title Replacement")
-                onClicked: pushNewReplacement()
+                onClicked: replacementsModel.pushNewReplacement()
             }
             Label {
                 text: i18n("<a href=\"https://www.w3schools.com/jsref/jsref_obj_regexp.asp\">JavaScript RegExp Reference</a>")
@@ -143,41 +255,32 @@ KCM.SimpleKCM {
             }
 
             Connections {
-                target: page
+                target: replacementsModel
 
-                function onCfg_titleReplacementsTypesChanged() {
+                function onDataChanged() {
                     testOutput.updateTestOutput();
                 }
 
-                function onCfg_titleReplacementsPatternsChanged() {
+                function onRowsRemoved() {
                     testOutput.updateTestOutput();
                 }
+            }
 
-                function onCfg_titleReplacementsTemplatesChanged() {
-                    testOutput.updateTestOutput();
+            function _updateTestOutput() {
+                let outputText = testInput.text;
+                for (let i = 0; i < replacementsModel.count; i++) {
+                    const rowValue = replacementsModel.get(i);
+                    const replacement = Utils.Replacement.createReplacement(rowValue.type, rowValue.pattern, rowValue.template);
+                    outputText = replacement.replace(outputText);
                 }
+                testOutput.text = outputText;
             }
 
             function updateTestOutput() {
-                const replacements = Utils.Replacement.createReplacementList(cfg_titleReplacementsTypes, cfg_titleReplacementsPatterns, cfg_titleReplacementsTemplates);
-                testOutput.text = Utils.Replacement.applyReplacementList(testInput.text, replacements);
+                Qt.callLater(_updateTestOutput);
             }
 
             Component.onCompleted: updateTestOutput()
         }
     }
-
-    function pushNewReplacement() {
-        cfg_titleReplacementsTypes.push(TitleReplacements.Type.String);
-        cfg_titleReplacementsTemplates.push("");
-        cfg_titleReplacementsPatterns.push("");
-        replacementsRepeater.updateModel();
-    }
-
-    function deleteReplacement(index) {
-        cfg_titleReplacementsPatterns.splice(index, 1);
-        cfg_titleReplacementsTypes.splice(index, 1);
-        cfg_titleReplacementsTemplates.splice(index, 1);
-        replacementsRepeater.updateModel();
-    }
 }
diff --git a/package/contents/ui/main.qml b/package/contents/ui/main.qml
index 1942651..afde222 100644
--- a/package/contents/ui/main.qml
+++ b/package/contents/ui/main.qml
@@ -183,32 +183,10 @@ PlasmoidItem {
             property bool empty: text === undefined || text === ""
             property bool hideEmpty: empty && plasmoid.configuration.windowTitleHideEmpty
             property int windowTitleSource: plasmoid.configuration.overrideElementsMaximized && tasksModel.activeWindow.maximized ? plasmoid.configuration.windowTitleSourceMaximized : plasmoid.configuration.windowTitleSource
-            property var titleTextReplacements: Utils.Replacement.createReplacementList(plasmoid.configuration.titleReplacementsTypes, plasmoid.configuration.titleReplacementsPatterns, plasmoid.configuration.titleReplacementsTemplates)
-
-            function titleText(windowTitleSource) {
-                let titleTextResult = "";
-                switch (windowTitleSource) {
-                case 0:
-                    titleTextResult = tasksModel.activeWindow.appName;
-                    break;
-                case 1:
-                    titleTextResult = tasksModel.activeWindow.decoration;
-                    break;
-                case 2:
-                    titleTextResult = tasksModel.activeWindow.genericAppName;
-                    break;
-                case 3:
-                    titleTextResult = plasmoid.configuration.windowTitleUndefined;
-                    break;
-                }
-                if (titleTextResult) {
-                    titleTextResult = Utils.Replacement.applyReplacementList(titleTextResult, titleTextReplacements);
-                }
-                return titleTextResult;
-            }
+            property var titleTextReplacements: []
 
             Layout.leftMargin: !hideEmpty ? plasmoid.configuration.windowTitleMarginsLeft : 0
-            Layout.topMargin: !hideEmpty ? plasmoid.configuration.windowTitleMarginsTop : 0
+            Layout.topMargin: !hideEmpty ? plasmoid.configuration.windowTitleMarginsTotitleReplacementsTypesp : 0
             Layout.bottomMargin: !hideEmpty ? plasmoid.configuration.windowTitleMarginsBottom : 0
             Layout.rightMargin: !hideEmpty ? plasmoid.configuration.windowTitleMarginsRight : 0
             Layout.minimumWidth: plasmoid.configuration.windowTitleMinimumWidth
@@ -224,6 +202,22 @@ PlasmoidItem {
             wrapMode: Text.WrapAnywhere
             enabled: tasksModel.hasActiveWindow
 
+            Connections {
+                target: plasmoid.configuration
+
+                function onTitleReplacementsTypesChanged() {
+                    updateTitleTextReplacements();
+                }
+
+                function onTitleReplacementsPatternsChanged() {
+                    updateTitleTextReplacements();
+                }
+
+                function onTitleReplacementsTemplatesChanged() {
+                    updateTitleTextReplacements();
+                }
+            }
+
             WidgetDragHandler {
                 Component.onCompleted: {
                     invokeKWinShortcut.connect(root.invokeKWinShortcut);
@@ -249,6 +243,36 @@ PlasmoidItem {
                     invokeKWinShortcut.connect(root.invokeKWinShortcut);
                 }
             }
+
+            function titleText(windowTitleSource) {
+                let titleTextResult = "";
+                switch (windowTitleSource) {
+                case 0:
+                    titleTextResult = tasksModel.activeWindow.appName;
+                    break;
+                case 1:
+                    titleTextResult = tasksModel.activeWindow.decoration;
+                    break;
+                case 2:
+                    titleTextResult = tasksModel.activeWindow.genericAppName;
+                    break;
+                case 3:
+                    titleTextResult = plasmoid.configuration.windowTitleUndefined;
+                    break;
+                }
+                if (titleTextResult) {
+                    titleTextResult = Utils.Replacement.applyReplacementList(titleTextResult, titleTextReplacements);
+                }
+                return titleTextResult;
+            }
+
+            function updateTitleTextReplacements() {
+                Qt.callLater(_updateTitleTextReplacements);
+            }
+
+            function _updateTitleTextReplacements() {
+                titleTextReplacements = Utils.Replacement.createReplacementList(plasmoid.configuration.titleReplacementsTypes, plasmoid.configuration.titleReplacementsPatterns, plasmoid.configuration.titleReplacementsTemplates);
+            }
         }
     }
 
diff --git a/package/contents/ui/utils.js b/package/contents/ui/utils.js
index 9387f88..b5abe44 100644
--- a/package/contents/ui/utils.js
+++ b/package/contents/ui/utils.js
@@ -120,13 +120,16 @@ class Replacement {
 
     static createReplacementList(types, patterns, templates) {
         const length = types.length;
-        if (length !== patterns.length || length !== templates.length) {
-            return [];
-        }
-
-        let result = new Array(length);
+        let result = new Array();
         for (let i = 0; i < length; i++) {
-            result[i] = Replacement.createReplacement(types[i], patterns[i], templates[i]);
+            const type = types[i];
+            const pattern = patterns[i];
+            const template = templates[i];
+            if (type === undefined || pattern === undefined || template === undefined) {
+                break; // Inconsistent state
+            } else {
+                result.push(Replacement.createReplacement(type, pattern, template));
+            }
         }
         return result;
     }