1/*
2 SPDX-FileCopyrightText: 2014 Marco Martin <mart@kde.org>
3 SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
4
5 SPDX-License-Identifier: LGPL-2.0-only
6*/
7
8#include "kcmoduleqml_p.h"
9
10#include <QQuickItem>
11#include <QQuickWidget>
12#include <QQuickWindow>
13#include <QTimer>
14#include <QVBoxLayout>
15
16#include <KAboutData>
17#include <KLocalizedContext>
18#include <KPageWidget>
19#include <QQmlEngine>
20
21#include "quick/kquickconfigmodule.h"
22
23#include <kcmutils_debug.h>
24#include <qquickitem.h>
25
26class QmlConfigModuleWidget;
27class KCModuleQmlPrivate
28{
29public:
30 KCModuleQmlPrivate(KQuickConfigModule *cm, KCModuleQml *qq)
31 : q(qq)
32 , configModule(std::move(cm))
33 {
34 }
35
36 ~KCModuleQmlPrivate()
37 {
38 }
39
40 void syncCurrentIndex()
41 {
42 if (!configModule || !pageRow) {
43 return;
44 }
45
46 configModule->setCurrentIndex(pageRow->property(name: "currentIndex").toInt());
47 }
48
49 KCModuleQml *q;
50 QQuickWindow *quickWindow = nullptr;
51 QQuickWidget *quickWidget = nullptr;
52 QQuickItem *rootPlaceHolder = nullptr;
53 QQuickItem *pageRow = nullptr;
54 KQuickConfigModule *configModule;
55 QmlConfigModuleWidget *widget = nullptr;
56};
57
58class QmlConfigModuleWidget : public QWidget
59{
60 Q_OBJECT
61public:
62 QmlConfigModuleWidget(KCModuleQml *module, QWidget *parent)
63 : QWidget(parent)
64 , m_module(module)
65 {
66 setFocusPolicy(Qt::StrongFocus);
67 }
68
69 QSize sizeHint() const override
70 {
71 if (!m_module->d->rootPlaceHolder) {
72 return QSize();
73 }
74
75 return QSize(m_module->d->rootPlaceHolder->implicitWidth(), m_module->d->rootPlaceHolder->implicitHeight());
76 }
77
78 bool eventFilter(QObject *watched, QEvent *event) override
79 {
80 // Everything would work mosty without manual intervention, but as of Qt 6.8
81 // things require special attention so that they work correctly with orca.
82 // The timing between the focusproxied QQuickWidget receiving focus and the
83 // focused qml Item being registered as focused is off and screen readers get
84 // confused. Instead, put initial focus on the root element and switch with a timer
85 // so the qml focuschange happens while the qquickwidget has focus. This
86 // requires activeFocusOnTab on the rootPlaceHolder to work, and that makes other things
87 // a bit messier than they would otherwise need to be.
88 if (event->type() == QEvent::FocusIn && watched == m_module->d->rootPlaceHolder) {
89 auto focusEvent = static_cast<QFocusEvent *>(event);
90 if (focusEvent->reason() == Qt::TabFocusReason) {
91 m_module->d->rootPlaceHolder->forceActiveFocus(reason: Qt::OtherFocusReason);
92 QTimer::singleShot(interval: 0, receiver: this, slot: [this] {
93 QQuickItem *nextItem = m_module->d->rootPlaceHolder->nextItemInFocusChain(forward: true);
94 if (nextItem) {
95 nextItem->forceActiveFocus(reason: Qt::TabFocusReason);
96 }
97 });
98 return true;
99 } else if (focusEvent->reason() == Qt::BacktabFocusReason) {
100 // this can either happen from backtabbing in qml or from backtabbing
101 // from qwidgets past the focusproxy (e.g. from the kcm buttons).
102 if (!m_module->d->rootPlaceHolder->hasActiveFocus()) {
103 // we're in widgets, enter qml from reverse in the focus chain in the same way as above
104 QTimer::singleShot(interval: 0, receiver: this, slot: [this] {
105 QQuickItem *nextItem = m_module->d->rootPlaceHolder->nextItemInFocusChain(forward: false);
106 if (nextItem) {
107 nextItem->forceActiveFocus(reason: Qt::TabFocusReason);
108 }
109 });
110 return true;
111 }
112 // we're coming from qml, so we focus the widget outside. This also needs singleShot;
113 // if we do it immediately, focus cycles backward along the qml focus chain instead.
114 // Without activeFocusOnTab on the rootPlaceHolder we could just return false and
115 // Qt would handle everything by itself
116 QTimer::singleShot(interval: 0, receiver: this, slot: [this] {
117 focusNextPrevChild(next: false);
118 });
119 return true;
120 }
121 }
122 return QWidget::eventFilter(watched, event);
123 }
124
125private:
126 KCModuleQml *m_module;
127};
128
129KCModuleQml::KCModuleQml(KQuickConfigModule *configModule, QWidget *parent)
130 : KCModule(parent, configModule->metaData())
131 , d(new KCModuleQmlPrivate(configModule, this))
132{
133 d->widget = new QmlConfigModuleWidget(this, parent);
134 setButtons(d->configModule->buttons());
135 connect(sender: d->configModule, signal: &KQuickConfigModule::buttonsChanged, context: d->configModule, slot: [this] {
136 setButtons(d->configModule->buttons());
137 });
138
139 setNeedsSave(d->configModule->needsSave());
140 connect(sender: d->configModule, signal: &KQuickConfigModule::needsSaveChanged, context: this, slot: [this] {
141 setNeedsSave(d->configModule->needsSave());
142 });
143
144 setRepresentsDefaults(d->configModule->representsDefaults());
145 connect(sender: d->configModule, signal: &KQuickConfigModule::representsDefaultsChanged, context: this, slot: [this] {
146 setRepresentsDefaults(d->configModule->representsDefaults());
147 });
148
149 setAuthActionName(d->configModule->authActionName());
150 connect(sender: d->configModule, signal: &KQuickConfigModule::authActionNameChanged, context: this, slot: [this] {
151 setAuthActionName(d->configModule->authActionName());
152 });
153
154 connect(sender: this, signal: &KCModule::defaultsIndicatorsVisibleChanged, context: d->configModule, slot: [this] {
155 d->configModule->setDefaultsIndicatorsVisible(defaultsIndicatorsVisible());
156 });
157
158 connect(sender: this, signal: &KAbstractConfigModule::activationRequested, context: d->configModule, slot: &KQuickConfigModule::activationRequested);
159
160 // Build the UI
161 QVBoxLayout *layout = new QVBoxLayout(d->widget);
162 layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
163
164 d->quickWidget = new QQuickWidget(d->configModule->engine().get(), d->widget);
165 d->quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
166 d->quickWidget->setAttribute(Qt::WA_AlwaysStackOnTop, on: true);
167 d->quickWidget->setAttribute(Qt::WA_NoMousePropagation, on: true); // Workaround for QTBUG-109861 to fix drag everywhere
168 d->quickWindow = d->quickWidget->quickWindow();
169 d->quickWindow->setColor(Qt::transparent);
170 d->widget->setFocusProxy(d->quickWidget);
171
172 QQmlComponent *component = new QQmlComponent(d->configModule->engine().get(), this);
173 // activeFocusOnTab is required to have screen readers not get confused
174 // pushPage/popPage are needed as push of StackView can't be directly invoked from c++
175 // because its parameters are QQmlV4Function which is not public.
176 // The managers of onEnter/ReturnPressed are a workaround of
177 // Qt bug https://bugreports.qt.io/browse/QTBUG-70934
178 // clang-format off
179 // TODO: move this in an instantiable component which would be used by the qml-only version as well
180 component->setData(QByteArrayLiteral(R"(
181import QtQuick
182import QtQuick.Controls as QQC2
183import org.kde.kirigami 2 as Kirigami
184import org.kde.kcmutils as KCMUtils
185
186Kirigami.ApplicationItem {
187 // force it to *never* try to resize itself
188 width: Window.width
189
190 implicitWidth: Math.max(pageStack.implicitWidth, Kirigami.Units.gridUnit * 36)
191 implicitHeight: Math.max(pageStack.implicitHeight, Kirigami.Units.gridUnit * 20)
192
193 activeFocusOnTab: true
194
195 property KCMUtils.ConfigModule kcm
196
197 QQC2.ToolButton {
198 id: toolButton
199 visible: false
200 icon.name: "go-previous"
201 }
202
203 pageStack.separatorVisible: pageStack.depth > 0 && (pageStack.items[0].sidebarMode ?? false)
204 pageStack.globalToolBar.preferredHeight: toolButton.implicitHeight + Kirigami.Units.smallSpacing * 2
205 pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
206 pageStack.globalToolBar.showNavigationButtons: Kirigami.ApplicationHeaderStyle.ShowBackButton
207
208 pageStack.columnView.columnResizeMode: pageStack.items.length > 0 && (pageStack.items[0].Kirigami.ColumnView.fillWidth || pageStack.items.filter(item => item.visible).length === 1)
209 ? Kirigami.ColumnView.SingleColumn
210 : Kirigami.ColumnView.FixedColumns
211
212 pageStack.defaultColumnWidth: kcm && kcm.columnWidth > 0 ? kcm.columnWidth : Kirigami.Units.gridUnit * 15
213
214 footer: null
215 Keys.onReturnPressed: event => {
216 event.accepted = true
217 }
218 Keys.onEnterPressed: event => {
219 event.accepted = true
220 }
221
222 Window.onWindowChanged: {
223 if (Window.window) {
224 Window.window.LayoutMirroring.enabled = Qt.binding(() => Qt.application.layoutDirection === Qt.RightToLeft)
225 Window.window.LayoutMirroring.childrenInherit = true
226 }
227 }
228}
229 )"), baseUrl: QUrl(QStringLiteral("kcmutils/kcmmoduleqml.cpp")));
230 // clang-format on
231
232 d->rootPlaceHolder = qobject_cast<QQuickItem *>(o: component->create());
233 if (!d->rootPlaceHolder) {
234 qCCritical(KCMUTILS_LOG) << component->errors();
235 qFatal(msg: "Failed to initialize KCModuleQML");
236 }
237 d->rootPlaceHolder->setProperty(name: "kcm", value: QVariant::fromValue(value: d->configModule));
238 d->rootPlaceHolder->installEventFilter(filterObj: d->widget);
239 d->quickWidget->setContent(url: QUrl(), component, item: d->rootPlaceHolder);
240
241 d->pageRow = d->rootPlaceHolder->property(name: "pageStack").value<QQuickItem *>();
242 if (d->pageRow) {
243 d->pageRow->setProperty(name: "initialPage", value: QVariant::fromValue(value: d->configModule->mainUi()));
244
245 for (int i = 0; i < d->configModule->depth() - 1; i++) {
246 QMetaObject::invokeMethod(obj: d->pageRow,
247 member: "push",
248 c: Qt::DirectConnection,
249 Q_ARG(QVariant, QVariant::fromValue(d->configModule->subPage(i))),
250 Q_ARG(QVariant, QVariant()));
251 if (d->configModule->mainUi()->property(name: "sidebarMode").toBool()) {
252 d->pageRow->setProperty(name: "currentIndex", value: 0);
253 d->configModule->setCurrentIndex(0);
254 }
255 }
256
257 connect(sender: d->configModule, signal: &KQuickConfigModule::pagePushed, context: this, slot: [this](QQuickItem *page) {
258 QMetaObject::invokeMethod(obj: d->pageRow, member: "push", c: Qt::DirectConnection, Q_ARG(QVariant, QVariant::fromValue(page)), Q_ARG(QVariant, QVariant()));
259 });
260 connect(sender: d->configModule, signal: &KQuickConfigModule::pageRemoved, context: this, slot: [this]() {
261 QMetaObject::invokeMethod(obj: d->pageRow, member: "pop", c: Qt::DirectConnection, Q_ARG(QVariant, QVariant()));
262 });
263 connect(sender: d->configModule, signal: &KQuickConfigModule::currentIndexChanged, context: this, slot: [this]() {
264 d->pageRow->setProperty(name: "currentIndex", value: d->configModule->currentIndex());
265 });
266 // New syntax cannot be used to connect to QML types
267 connect(sender: d->pageRow, SIGNAL(currentIndexChanged()), receiver: this, SLOT(syncCurrentIndex()));
268 }
269
270 layout->addWidget(d->quickWidget);
271}
272
273KCModuleQml::~KCModuleQml() = default;
274
275void KCModuleQml::load()
276{
277 KCModule::load(); // calls setNeedsSave(false)
278 d->configModule->load();
279}
280
281void KCModuleQml::save()
282{
283 d->configModule->save();
284 d->configModule->setNeedsSave(false);
285}
286
287void KCModuleQml::defaults()
288{
289 d->configModule->defaults();
290}
291
292QWidget *KCModuleQml::widget()
293{
294 return d->widget;
295}
296
297#include "kcmoduleqml.moc"
298#include "moc_kcmoduleqml_p.cpp"
299

source code of kcmutils/src/kcmoduleqml.cpp