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

source code of kcmutils/src/kcmoduleqml.cpp