File: upstream_6dc76c3d_kcmoduleqml-simplify-and-improve-focus-handling.patch

package info (click to toggle)
kf6-kcmutils 6.13.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 23,712 kB
  • sloc: cpp: 3,565; sh: 19; makefile: 7
file content (137 lines) | stat: -rw-r--r-- 6,962 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
From 6dc76c3dc14aaaecc926a0af8f092402b23bf22b Mon Sep 17 00:00:00 2001
From: Christoph Wolk <cwo.kde@posteo.net>
Date: Sat, 8 Feb 2025 18:21:50 +0100
Subject: [PATCH] kcmoduleqml: simplify and improve focus handling

kcmoduleqml contains some old tricks to make passing focus between the
qml and qtwidgets bits, but they are fragile, and break in many cases -
especially with systemsettings adding another layer of qml. The hacks
may have been necessary in the past, but things actually work well now,
so we can essentially just set a focusProxy and be done ... as long as
we don't care about accessibility. The qml bits receive focus before the
widgets parts fully hand it over, so the first qml item receiving focus
will not be  announced over screen readers. We do care though, and thus
some special handling is still needed.

We switch to using focusProxy, and add a bit of surgery to the focus
transition so that screen readers always stay in the loop about what
things are focused. We also need a bit of special handling for the
backtab case, which doesn't quite work automatically with
activeFocusOnTab set on the qml root.
---
 src/kcmoduleqml.cpp | 60 ++++++++++++++++++++++++++++-----------------
 1 file changed, 38 insertions(+), 22 deletions(-)

diff --git a/src/kcmoduleqml.cpp b/src/kcmoduleqml.cpp
index 5666029f..35ac1e54 100644
--- a/src/kcmoduleqml.cpp
+++ b/src/kcmoduleqml.cpp
@@ -10,6 +10,7 @@
 #include <QQuickItem>
 #include <QQuickWidget>
 #include <QQuickWindow>
+#include <QTimer>
 #include <QVBoxLayout>
 
 #include <KAboutData>
@@ -20,6 +21,7 @@
 #include "quick/kquickconfigmodule.h"
 
 #include <kcmutils_debug.h>
+#include <qquickitem.h>
 
 class QmlConfigModuleWidget;
 class KCModuleQmlPrivate
@@ -64,15 +66,6 @@ public:
         setFocusPolicy(Qt::StrongFocus);
     }
 
-    void focusInEvent(QFocusEvent *event) override
-    {
-        if (event->reason() == Qt::TabFocusReason) {
-            m_module->d->rootPlaceHolder->nextItemInFocusChain(true)->forceActiveFocus(Qt::TabFocusReason);
-        } else if (event->reason() == Qt::BacktabFocusReason) {
-            m_module->d->rootPlaceHolder->nextItemInFocusChain(false)->forceActiveFocus(Qt::BacktabFocusReason);
-        }
-    }
-
     QSize sizeHint() const override
     {
         if (!m_module->d->rootPlaceHolder) {
@@ -84,21 +77,45 @@ public:
 
     bool eventFilter(QObject *watched, QEvent *event) override
     {
-        if (watched == m_module->d->rootPlaceHolder && event->type() == QEvent::FocusIn) {
+        // Everything would work mosty without manual intervention, but as of Qt 6.8
+        // things require special attention so that they work correctly with orca.
+        // The timing between the focusproxied QQuickWidget receiving focus and the
+        // focused qml Item being registered as focused is off and screen readers get
+        // confused. Instead, put initial focus on the root element and switch with a timer
+        // so the qml focuschange happens while the qquickwidget has focus. This
+        // requires activeFocusOnTab on the rootPlaceHolder to work, and that makes other things
+        // a bit messier than they would otherwise need to be.
+        if (event->type() == QEvent::FocusIn && watched == m_module->d->rootPlaceHolder) {
             auto focusEvent = static_cast<QFocusEvent *>(event);
             if (focusEvent->reason() == Qt::TabFocusReason) {
-                QWidget *w = m_module->d->quickWidget->nextInFocusChain();
-                while (!w->isEnabled() || !(w->focusPolicy() & Qt::TabFocus)) {
-                    w = w->nextInFocusChain();
-                }
-                w->setFocus(Qt::TabFocusReason); // allow tab navigation inside the qquickwidget
+                m_module->d->rootPlaceHolder->forceActiveFocus(Qt::OtherFocusReason);
+                QTimer::singleShot(0, this, [this] {
+                    QQuickItem *nextItem = m_module->d->rootPlaceHolder->nextItemInFocusChain(true);
+                    if (nextItem) {
+                        nextItem->forceActiveFocus(Qt::TabFocusReason);
+                    }
+                });
                 return true;
             } else if (focusEvent->reason() == Qt::BacktabFocusReason) {
-                QWidget *w = m_module->d->quickWidget->previousInFocusChain();
-                while (!w->isEnabled() || !(w->focusPolicy() & Qt::TabFocus)) {
-                    w = w->previousInFocusChain();
+                // this can either happen from backtabbing in qml or from backtabbing
+                // from qwidgets past the focusproxy (e.g. from the kcm buttons).
+                if (!m_module->d->rootPlaceHolder->hasActiveFocus()) {
+                    // we're in widgets, enter qml from reverse in the focus chain in the same way as above
+                    QTimer::singleShot(0, this, [this] {
+                        QQuickItem *nextItem = m_module->d->rootPlaceHolder->nextItemInFocusChain(false);
+                        if (nextItem) {
+                            nextItem->forceActiveFocus(Qt::TabFocusReason);
+                        }
+                    });
+                    return true;
                 }
-                w->setFocus(Qt::BacktabFocusReason);
+                // we're coming from qml, so we focus the widget outside. This also needs singleShot;
+                // if we do it immediately, focus cycles backward along the qml focus chain instead.
+                // Without activeFocusOnTab on the rootPlaceHolder we could just return false and
+                // Qt would handle everything by itself
+                QTimer::singleShot(0, this, [this] {
+                    focusNextPrevChild(false);
+                });
                 return true;
             }
         }
@@ -146,15 +163,14 @@ KCModuleQml::KCModuleQml(KQuickConfigModule *configModule, QWidget *parent)
 
     d->quickWidget = new QQuickWidget(d->configModule->engine().get(), d->widget);
     d->quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
-    d->quickWidget->setFocusPolicy(Qt::StrongFocus);
     d->quickWidget->setAttribute(Qt::WA_AlwaysStackOnTop, true);
     d->quickWidget->setAttribute(Qt::WA_NoMousePropagation, true); // Workaround for QTBUG-109861 to fix drag everywhere
     d->quickWindow = d->quickWidget->quickWindow();
     d->quickWindow->setColor(Qt::transparent);
+    d->widget->setFocusProxy(d->quickWidget);
 
     QQmlComponent *component = new QQmlComponent(d->configModule->engine().get(), this);
-    // this has activeFocusOnTab to notice when the navigation wraps
-    // around, so when we need to go outside and inside
+    // activeFocusOnTab is required to have screen readers not get confused
     // pushPage/popPage are needed as push of StackView can't be directly invoked from c++
     // because its parameters are QQmlV4Function which is not public.
     // The managers of onEnter/ReturnPressed are a workaround of
-- 
GitLab