[plasma/kscreen] kcm: kcm: allow moving the displays pixel-by-pixel using arrow keys

Xaver Hugl <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit c7b9b8b5f94f6caf3ff38f61eb9672d6921d8617 by Xaver Hugl, on behalf of Antti Savolainen.
Committed on 31/07/2026 at 17:33.
Pushed by zamundaaa into branch 'master'.

kcm: allow moving the displays pixel-by-pixel using arrow keys

Also fade out the badge when the coordinates are shown

BUG: 519347
BUG: 521731

M  +27   -10   kcm/output_model.cpp
M  +1    -0    kcm/output_model.h
M  +46   -2    kcm/ui/Output.qml

https://invent.kde.org/plasma/kscreen/-/commit/c7b9b8b5f94f6caf3ff38f61eb9672d6921d8617

diff --git a/kcm/output_model.cpp b/kcm/output_model.cpp
index 723a91ac..1e866160 100644
--- a/kcm/output_model.cpp
+++ b/kcm/output_model.cpp
@@ -178,6 +178,32 @@ QVariant OutputModel::data(const QModelIndex &index, int role) const
     return QVariant();
 }
 
+bool OutputModel::setPosition(int outputIndex, const QPoint &valToPoint, bool snapEnabled)
+{
+    if (outputIndex < 0 || outputIndex >= m_outputs.count()) {
+        return false;
+    }
+
+    Output &output = m_outputs[outputIndex];
+    QPoint val = valToPoint;
+
+    if (output.pos == val) {
+        return false;
+    }
+
+    if (snapEnabled) {
+        snap(output, val);
+    }
+
+    output.pos = val;
+    updatePositions();
+
+    Q_EMIT positionChanged();
+    const QModelIndex index = createIndex(outputIndex, 0);
+    Q_EMIT dataChanged(index, index, {PositionRole});
+    return true;
+}
+
 bool OutputModel::setData(const QModelIndex &index, const QVariant &value, int role)
 {
     if (index.row() < 0 || index.row() >= m_outputs.count()) {
@@ -188,16 +214,7 @@ bool OutputModel::setData(const QModelIndex &index, const QVariant &value, int r
     switch (role) {
     case PositionRole:
         if (value.canConvert<QPoint>()) {
-            QPoint val = value.toPoint();
-            if (output.pos == val) {
-                return false;
-            }
-            snap(output, val);
-            m_outputs[index.row()].pos = val;
-            updatePositions();
-            Q_EMIT positionChanged();
-            Q_EMIT dataChanged(index, index, {role});
-            return true;
+            return setPosition(index.row(), value.toPoint());
         }
         break;
     case EnabledRole:
diff --git a/kcm/output_model.h b/kcm/output_model.h
index 8456b2bb..8eb1d994 100644
--- a/kcm/output_model.h
+++ b/kcm/output_model.h
@@ -93,6 +93,7 @@ public:
     bool positionsNormalized() const;
 
     bool isMoving() const;
+    Q_INVOKABLE bool setPosition(int outputIndex, const QPoint &valToPoint, bool snapEnabled = true);
 
 Q_SIGNALS:
     void positionChanged();
diff --git a/kcm/ui/Output.qml b/kcm/ui/Output.qml
index d91fcbf9..7ef17f81 100644
--- a/kcm/ui/Output.qml
+++ b/kcm/ui/Output.qml
@@ -22,6 +22,7 @@ Item {
                                          model.resolution.width.toString(), model.resolution.height.toString(), Math.round(model.scale * 100.0))
     property string textWithoutScale: i18nc("Width, height; separated with no-break space", "(%1 × %2)",
                                             model.resolution.width.toString(), model.resolution.height.toString())
+    property bool arrowKeyHeld: false
 
     onIsSelectedChanged: {
         if (isSelected) {
@@ -122,7 +123,7 @@ Item {
                 Layout.leftMargin: Kirigami.Units.smallSpacing
                 Layout.bottomMargin: Kirigami.Units.smallSpacing
                 number: model.numberByConnector
-                visible: labelContainer.height >= implicitHeight + Kirigami.Units.smallSpacing * 2
+                visible: labelContainer.height >= implicitHeight + Kirigami.Units.smallSpacing * 2 && !(tapHandler.isLongPressed || dragHandler.active || output.arrowKeyHeld)
             }
 
             QQC2.Label {
@@ -221,7 +222,7 @@ Item {
         radius: Kirigami.Units.cornerRadius
 
         opacity: model.enabled &&
-                 (tapHandler.isLongPressed || dragHandler.active) ? 0.9 : 0.0
+                 (tapHandler.isLongPressed || dragHandler.active || output.arrowKeyHeld) ? 0.9 : 0.0
 
 
         color: Kirigami.Theme.disabledTextColor
@@ -282,6 +283,7 @@ Item {
         onPressedChanged: {
             if (pressed) {
                 root.selectedOutput = model.index;
+                output.forceActiveFocus()
                 dragStartPosition = Qt.point(output.x, output.y)
             } else {
                 isLongPressed = false;
@@ -309,5 +311,47 @@ Item {
             }
         }
     }
+
+    Timer {
+        id: uiLinger
+        interval: 600
+        onTriggered: {
+            arrowKeyHeld = false
+        }
+    }
+
+    Keys.onPressed: (event) => {
+        if (!output.interactive) {
+            return
+        }
+
+        uiLinger.stop()
+        const delta = Qt.point(0, 0);
+        if (event.key === Qt.Key_Left) {
+            delta.x = -1
+            arrowKeyHeld = true
+            model.interactiveMove = true;
+        } else if (event.key === Qt.Key_Right) {
+            delta.x = 1
+            arrowKeyHeld = true
+            model.interactiveMove = true;
+        } else if (event.key === Qt.Key_Up) {
+            delta.y = -1
+            arrowKeyHeld = true
+            model.interactiveMove = true;
+        } else if (event.key === Qt.Key_Down) {
+            delta.y = 1
+            arrowKeyHeld = true
+            model.interactiveMove = true;
+        }
+        kcm.outputModel.sourceModel.setPosition(model.index, Qt.point(model.position.x + delta.x, model.position.y + delta.y), false)
+    }
+    Keys.onReleased: (event) => {
+        if (event.isAutoRepeat) {
+            return
+        }
+        model.interactiveMove = false
+        uiLinger.start()
+    }
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.