File indexing completed on 2024-05-05 03:53:27

0001 /*
0002     SPDX-FileCopyrightText: 2014 Marco Martin <mart@kde.org>
0003     SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "kcmoduleqml_p.h"
0009 
0010 #include <QQuickItem>
0011 #include <QQuickWidget>
0012 #include <QQuickWindow>
0013 #include <QVBoxLayout>
0014 
0015 #include <KAboutData>
0016 #include <KLocalizedContext>
0017 #include <KPageWidget>
0018 #include <QQmlEngine>
0019 
0020 #include "qml/kquickconfigmodule.h"
0021 
0022 #include <kcmutils_debug.h>
0023 
0024 class QmlConfigModuleWidget;
0025 class KCModuleQmlPrivate
0026 {
0027 public:
0028     KCModuleQmlPrivate(KQuickConfigModule *cm, KCModuleQml *qq)
0029         : q(qq)
0030         , configModule(std::move(cm))
0031     {
0032     }
0033 
0034     ~KCModuleQmlPrivate()
0035     {
0036     }
0037 
0038     void syncCurrentIndex()
0039     {
0040         if (!configModule || !pageRow) {
0041             return;
0042         }
0043 
0044         configModule->setCurrentIndex(pageRow->property("currentIndex").toInt());
0045     }
0046 
0047     KCModuleQml *q;
0048     QQuickWindow *quickWindow = nullptr;
0049     QQuickWidget *quickWidget = nullptr;
0050     QQuickItem *rootPlaceHolder = nullptr;
0051     QQuickItem *pageRow = nullptr;
0052     KQuickConfigModule *configModule;
0053     QmlConfigModuleWidget *widget = nullptr;
0054 };
0055 
0056 class QmlConfigModuleWidget : public QWidget
0057 {
0058     Q_OBJECT
0059 public:
0060     QmlConfigModuleWidget(KCModuleQml *module, QWidget *parent)
0061         : QWidget(parent)
0062         , m_module(module)
0063     {
0064         setFocusPolicy(Qt::StrongFocus);
0065     }
0066 
0067     void focusInEvent(QFocusEvent *event) override
0068     {
0069         if (event->reason() == Qt::TabFocusReason) {
0070             m_module->d->rootPlaceHolder->nextItemInFocusChain(true)->forceActiveFocus(Qt::TabFocusReason);
0071         } else if (event->reason() == Qt::BacktabFocusReason) {
0072             m_module->d->rootPlaceHolder->nextItemInFocusChain(false)->forceActiveFocus(Qt::BacktabFocusReason);
0073         }
0074     }
0075 
0076     QSize sizeHint() const override
0077     {
0078         if (!m_module->d->rootPlaceHolder) {
0079             return QSize();
0080         }
0081 
0082         return QSize(m_module->d->rootPlaceHolder->implicitWidth(), m_module->d->rootPlaceHolder->implicitHeight());
0083     }
0084 
0085     bool eventFilter(QObject *watched, QEvent *event) override
0086     {
0087         if (watched == m_module->d->rootPlaceHolder && event->type() == QEvent::FocusIn) {
0088             auto focusEvent = static_cast<QFocusEvent *>(event);
0089             if (focusEvent->reason() == Qt::TabFocusReason) {
0090                 QWidget *w = m_module->d->quickWidget->nextInFocusChain();
0091                 while (!w->isEnabled() || !(w->focusPolicy() & Qt::TabFocus)) {
0092                     w = w->nextInFocusChain();
0093                 }
0094                 w->setFocus(Qt::TabFocusReason); // allow tab navigation inside the qquickwidget
0095                 return true;
0096             } else if (focusEvent->reason() == Qt::BacktabFocusReason) {
0097                 QWidget *w = m_module->d->quickWidget->previousInFocusChain();
0098                 while (!w->isEnabled() || !(w->focusPolicy() & Qt::TabFocus)) {
0099                     w = w->previousInFocusChain();
0100                 }
0101                 w->setFocus(Qt::BacktabFocusReason);
0102                 return true;
0103             }
0104         }
0105         return QWidget::eventFilter(watched, event);
0106     }
0107 
0108 private:
0109     KCModuleQml *m_module;
0110 };
0111 
0112 KCModuleQml::KCModuleQml(KQuickConfigModule *configModule, QWidget *parent)
0113     : KCModule(parent, configModule->metaData())
0114     , d(new KCModuleQmlPrivate(configModule, this))
0115 {
0116     d->widget = new QmlConfigModuleWidget(this, parent);
0117     setButtons(d->configModule->buttons());
0118     connect(d->configModule, &KQuickConfigModule::buttonsChanged, d->configModule, [this] {
0119         setButtons(d->configModule->buttons());
0120     });
0121 
0122     setNeedsSave(d->configModule->needsSave());
0123     connect(d->configModule, &KQuickConfigModule::needsSaveChanged, this, [this] {
0124         setNeedsSave(d->configModule->needsSave());
0125     });
0126 
0127     setRepresentsDefaults(d->configModule->representsDefaults());
0128     connect(d->configModule, &KQuickConfigModule::representsDefaultsChanged, this, [this] {
0129         setRepresentsDefaults(d->configModule->representsDefaults());
0130     });
0131 
0132     setAuthActionName(d->configModule->authActionName());
0133     connect(d->configModule, &KQuickConfigModule::authActionNameChanged, this, [this] {
0134         setAuthActionName(d->configModule->authActionName());
0135     });
0136 
0137     connect(this, &KCModule::defaultsIndicatorsVisibleChanged, d->configModule, [this] {
0138         d->configModule->setDefaultsIndicatorsVisible(defaultsIndicatorsVisible());
0139     });
0140 
0141     connect(this, &KAbstractConfigModule::activationRequested, d->configModule, &KQuickConfigModule::activationRequested);
0142 
0143     // Build the UI
0144     QVBoxLayout *layout = new QVBoxLayout(d->widget);
0145     layout->setContentsMargins(0, 0, 0, 0);
0146 
0147     d->quickWidget = new QQuickWidget(d->configModule->engine().get(), d->widget);
0148     d->quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
0149     d->quickWidget->setFocusPolicy(Qt::StrongFocus);
0150     d->quickWidget->setAttribute(Qt::WA_AlwaysStackOnTop, true);
0151     d->quickWidget->setAttribute(Qt::WA_NoMousePropagation, true); // Workaround for QTBUG-109861 to fix drag everywhere
0152     d->quickWindow = d->quickWidget->quickWindow();
0153     d->quickWindow->setColor(Qt::transparent);
0154 
0155     QQmlComponent *component = new QQmlComponent(d->configModule->engine().get(), this);
0156     // this has activeFocusOnTab to notice when the navigation wraps
0157     // around, so when we need to go outside and inside
0158     // pushPage/popPage are needed as push of StackView can't be directly invoked from c++
0159     // because its parameters are QQmlV4Function which is not public.
0160     // The managers of onEnter/ReturnPressed are a workaround of
0161     // Qt bug https://bugreports.qt.io/browse/QTBUG-70934
0162     // clang-format off
0163     // TODO: move this in an instantiable component which would be used by the qml-only version as well
0164     component->setData(QByteArrayLiteral(R"(
0165 import QtQuick
0166 import QtQuick.Controls as QQC2
0167 import org.kde.kirigami 2 as Kirigami
0168 import org.kde.kcmutils as KCMUtils
0169 
0170 Kirigami.ApplicationItem {
0171     // force it to *never* try to resize itself
0172     width: Window.width
0173 
0174     implicitWidth: Math.max(pageStack.implicitWidth, Kirigami.Units.gridUnit * 36)
0175     implicitHeight: Math.max(pageStack.implicitHeight, Kirigami.Units.gridUnit * 20)
0176 
0177     activeFocusOnTab: true
0178 
0179     property KCMUtils.ConfigModule kcm
0180 
0181     QQC2.ToolButton {
0182         id: toolButton
0183         visible: false
0184         icon.name: "go-previous"
0185     }
0186 
0187     pageStack.separatorVisible: pageStack.depth > 0 && pageStack.items[0].sidebarMode
0188     pageStack.globalToolBar.preferredHeight: toolButton.implicitHeight + Kirigami.Units.smallSpacing * 2
0189     pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
0190     pageStack.globalToolBar.showNavigationButtons: pageStack.currentIndex > 0 ? Kirigami.ApplicationHeaderStyle.ShowBackButton : Kirigami.ApplicationHeaderStyle.NoNavigationButtons
0191 
0192     pageStack.columnView.columnResizeMode: pageStack.items.length > 0 && (pageStack.items[0].Kirigami.ColumnView.fillWidth || pageStack.items.filter(item => item.visible).length === 1)
0193         ? Kirigami.ColumnView.SingleColumn
0194         : Kirigami.ColumnView.FixedColumns
0195 
0196     pageStack.defaultColumnWidth: kcm && kcm.columnWidth > 0 ? kcm.columnWidth : Kirigami.Units.gridUnit * 15
0197 
0198     footer: null
0199     Keys.onReturnPressed: event => {
0200         event.accepted = true
0201     }
0202     Keys.onEnterPressed: event => {
0203         event.accepted = true
0204     }
0205 
0206     Window.onWindowChanged: {
0207         if (Window.window) {
0208             Window.window.LayoutMirroring.enabled = Qt.binding(() => Qt.application.layoutDirection === Qt.RightToLeft)
0209             Window.window.LayoutMirroring.childrenInherit = true
0210         }
0211     }
0212 }
0213     )"), QUrl(QStringLiteral("kcmutils/kcmmoduleqml.cpp")));
0214     // clang-format on
0215 
0216     d->rootPlaceHolder = qobject_cast<QQuickItem *>(component->create());
0217     if (!d->rootPlaceHolder) {
0218         qCCritical(KCMUTILS_LOG) << component->errors();
0219         qFatal("Failed to initialize KCModuleQML");
0220     }
0221     d->rootPlaceHolder->setProperty("kcm", QVariant::fromValue(d->configModule));
0222     d->rootPlaceHolder->installEventFilter(d->widget);
0223     d->quickWidget->setContent(QUrl(), component, d->rootPlaceHolder);
0224 
0225     d->pageRow = d->rootPlaceHolder->property("pageStack").value<QQuickItem *>();
0226     if (d->pageRow) {
0227         d->pageRow->setProperty("initialPage", QVariant::fromValue(d->configModule->mainUi()));
0228 
0229         for (int i = 0; i < d->configModule->depth() - 1; i++) {
0230             QMetaObject::invokeMethod(d->pageRow,
0231                                       "push",
0232                                       Qt::DirectConnection,
0233                                       Q_ARG(QVariant, QVariant::fromValue(d->configModule->subPage(i))),
0234                                       Q_ARG(QVariant, QVariant()));
0235             if (d->configModule->mainUi()->property("sidebarMode").toBool()) {
0236                 d->pageRow->setProperty("currentIndex", 0);
0237                 d->configModule->setCurrentIndex(0);
0238             }
0239         }
0240 
0241         connect(d->configModule, &KQuickConfigModule::pagePushed, this, [this](QQuickItem *page) {
0242             QMetaObject::invokeMethod(d->pageRow, "push", Qt::DirectConnection, Q_ARG(QVariant, QVariant::fromValue(page)), Q_ARG(QVariant, QVariant()));
0243         });
0244         connect(d->configModule, &KQuickConfigModule::pageRemoved, this, [this]() {
0245             QMetaObject::invokeMethod(d->pageRow, "pop", Qt::DirectConnection, Q_ARG(QVariant, QVariant()));
0246         });
0247         connect(d->configModule, &KQuickConfigModule::currentIndexChanged, this, [this]() {
0248             d->pageRow->setProperty("currentIndex", d->configModule->currentIndex());
0249         });
0250         // New syntax cannot be used to connect to QML types
0251         connect(d->pageRow, SIGNAL(currentIndexChanged()), this, SLOT(syncCurrentIndex()));
0252     }
0253 
0254     layout->addWidget(d->quickWidget);
0255 }
0256 
0257 KCModuleQml::~KCModuleQml() = default;
0258 
0259 void KCModuleQml::load()
0260 {
0261     KCModule::load(); // calls setNeedsSave(false)
0262     d->configModule->load();
0263 }
0264 
0265 void KCModuleQml::save()
0266 {
0267     d->configModule->save();
0268     d->configModule->setNeedsSave(false);
0269 }
0270 
0271 void KCModuleQml::defaults()
0272 {
0273     d->configModule->defaults();
0274 }
0275 
0276 QWidget *KCModuleQml::widget()
0277 {
0278     return d->widget;
0279 }
0280 
0281 #include "kcmoduleqml.moc"
0282 #include "moc_kcmoduleqml_p.cpp"