File indexing completed on 2024-05-05 17:45:46

0001 /*
0002  *   SPDX-FileCopyrightText: 2009 Ben Cooksley <bcooksley@kde.org>
0003  *   SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
0004  *   SPDX-FileCopyrightText: 2022 ivan tkachenko <me@ratijas.tk>
0005  *
0006  *   SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "SettingsBase.h"
0010 #include "../core/kcmmetadatahelpers.h"
0011 #include "BaseConfig.h"
0012 #include "systemsettings_app_debug.h"
0013 
0014 #include <QDir>
0015 #include <QFileInfo>
0016 #include <QFontDatabase>
0017 #include <QGuiApplication>
0018 #include <QLoggingCategory>
0019 #include <QMenu>
0020 #include <QMenuBar>
0021 #include <QRadioButton>
0022 #include <QScreen>
0023 #include <QTimer>
0024 #include <QVariantList>
0025 #include <QtGlobal>
0026 
0027 #include <KAboutData>
0028 #include <KActionCollection>
0029 #include <KCModuleInfo>
0030 #include <KConfigGroup>
0031 #include <KDesktopFile>
0032 #include <KFileUtils>
0033 #include <KIO/JobUiDelegateFactory>
0034 #include <KIO/OpenUrlJob>
0035 #include <KLocalizedString>
0036 #include <KMessageBox>
0037 #include <KPluginMetaData>
0038 #include <KStandardAction>
0039 #include <KToolBar>
0040 #include <KWindowConfig>
0041 #include <KXMLGUIFactory>
0042 
0043 #include "BaseData.h"
0044 #include "ModuleView.h"
0045 
0046 SettingsBase::SettingsBase(BaseMode::ApplicationMode mode, QWidget *parent)
0047     : KXmlGuiWindow(parent)
0048     , m_mode(mode)
0049 {
0050     // Ensure delayed loading doesn't cause a crash
0051     activeView = nullptr;
0052     aboutDialog = nullptr;
0053     lostFound = nullptr;
0054     // Prepare the view area
0055     stackedWidget = new QStackedWidget(this);
0056     setCentralWidget(stackedWidget);
0057     // Initialise search
0058     searchText = new KLineEdit(this);
0059     searchText->setClearButtonEnabled(true);
0060     searchText->setPlaceholderText(i18nc("Search through a list of control modules", "Search"));
0061     searchText->setCompletionMode(KCompletion::CompletionPopup);
0062 
0063     setProperty("_breeze_no_separator", true);
0064 
0065     if (m_mode == BaseMode::InfoCenter) {
0066         setWindowTitle(i18nd("systemsettings", "Info Center"));
0067         setWindowIcon(QIcon::fromTheme(QStringLiteral("hwinfo")));
0068     } else {
0069         setWindowTitle(i18nd("systemsettings", "System Settings"));
0070         setWindowIcon(QIcon::fromTheme(QStringLiteral("preferences-system")));
0071     }
0072 
0073     spacerWidget = new QWidget(this);
0074     spacerWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
0075     // Initialise the window so we don't flicker
0076     initToolBar();
0077     // We can now launch the delayed loading safely
0078     QTimer::singleShot(0, this, &SettingsBase::initApplication);
0079 }
0080 
0081 SettingsBase::~SettingsBase()
0082 {
0083     delete rootModule;
0084 }
0085 
0086 QSize SettingsBase::sizeHint() const
0087 {
0088     // Take the font size into account for the window size, as we do for UI elements
0089     const float fontSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSizeF();
0090     const QSize targetSize = QSize(qRound(102 * fontSize), qRound(70 * fontSize));
0091 
0092     // on smaller or portrait-rotated screens, do not max out height and/or width
0093     const QSize screenSize = (QGuiApplication::primaryScreen()->availableSize() * 0.9);
0094     return targetSize.boundedTo(screenSize);
0095 }
0096 
0097 void SettingsBase::initApplication()
0098 {
0099     // Prepare the menu of all modules
0100     auto source = m_mode == BaseMode::InfoCenter ? MetaDataSource::KInfoCenter : MetaDataSource::SystemSettings;
0101     pluginModules = findKCMsMetaData(source) << findExternalKCMModules(source);
0102 
0103     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("categories"), QStandardPaths::LocateDirectory);
0104     categories = KFileUtils::findAllUniqueFiles(dirs, QStringList(QStringLiteral("*.desktop")));
0105 
0106     rootModule = new MenuItem(true, nullptr);
0107     initMenuList(rootModule);
0108 
0109     // Handle lost+found modules...
0110     if (lostFound) {
0111         for (const auto &metaData : qAsConst(pluginModules)) {
0112             auto infoItem = new MenuItem(false, lostFound);
0113             infoItem->setMetaData(metaData);
0114             qCDebug(SYSTEMSETTINGS_APP_LOG) << "Added " << metaData.pluginId();
0115         }
0116     }
0117 
0118     // Prepare the Base Data
0119     BaseData::instance()->setMenuItem(rootModule);
0120     BaseData::instance()->setHomeItem(homeModule);
0121     // Only load the current used view
0122     m_plugins = KPluginMetaData::findPlugins(QStringLiteral("systemsettingsview/"));
0123     loadCurrentView();
0124 
0125     searchText->completionObject()->setIgnoreCase(true);
0126     searchText->completionObject()->setItems(BaseData::instance()->menuItem()->keywords());
0127     changePlugin();
0128 
0129     // enforce minimum window size
0130     setMinimumSize(SettingsBase::sizeHint());
0131     activateWindow();
0132 
0133     // Change size limit after screen resolution is changed
0134     m_screen = qGuiApp->primaryScreen();
0135     connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, [this](QScreen *screen) {
0136         if (m_screen) {
0137             disconnect(m_screen, &QScreen::geometryChanged, this, &SettingsBase::slotGeometryChanged);
0138         }
0139         m_screen = screen;
0140         slotGeometryChanged();
0141         connect(m_screen, &QScreen::geometryChanged, this, &SettingsBase::slotGeometryChanged);
0142     });
0143     connect(m_screen, &QScreen::geometryChanged, this, &SettingsBase::slotGeometryChanged);
0144 }
0145 
0146 void SettingsBase::initToolBar()
0147 {
0148     // Fill the toolbar with default actions
0149     // Exit is the very last action
0150     quitAction = actionCollection()->addAction(KStandardAction::Quit, QStringLiteral("quit_action"), this, &QWidget::close);
0151 
0152     if (m_mode == BaseMode::SystemSettings) {
0153         switchToIconAction = actionCollection()->addAction(QStringLiteral("switchto_iconview"), this, [this] {
0154             BaseConfig::setActiveView(QStringLiteral("systemsettings_icon_mode"));
0155             changePlugin();
0156         });
0157         switchToIconAction->setText(i18nd("systemsettings", "Switch to Icon View"));
0158         switchToIconAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-icons")));
0159 
0160         switchToSidebarAction = actionCollection()->addAction(QStringLiteral("switchto_sidebar"), this, [this] {
0161             BaseConfig::setActiveView(QStringLiteral("systemsettings_sidebar_mode"));
0162             changePlugin();
0163         });
0164         switchToSidebarAction->setText(i18nd("systemsettings", "Switch to Sidebar View"));
0165         switchToSidebarAction->setIcon(QIcon::fromTheme(QStringLiteral("view-sidetree")));
0166 
0167         highlightChangesAction = actionCollection()->addAction(QStringLiteral("highlight_changes"), this, [this] {
0168             if (activeView) {
0169                 activeView->toggleDefaultsIndicatorsVisibility();
0170             }
0171         });
0172         highlightChangesAction->setCheckable(true);
0173         highlightChangesAction->setText(i18nd("systemsettings", "Highlight Changed Settings"));
0174         highlightChangesAction->setIcon(QIcon::fromTheme(QStringLiteral("draw-highlight")));
0175     }
0176 
0177     reportPageSpecificBugAction = actionCollection()->addAction(QStringLiteral("report_bug_in_current_module"), this, [=] {
0178         const QString bugReportUrlString =
0179             activeView->moduleView()->activeModuleMetadata().bugReportUrl() + QStringLiteral("&version=") + QGuiApplication::applicationVersion();
0180         auto job = new KIO::OpenUrlJob(QUrl(bugReportUrlString));
0181         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
0182         job->start();
0183     });
0184     reportPageSpecificBugAction->setText(i18nd("systemsettings", "Report a Bug in the Current Pageā€¦"));
0185     reportPageSpecificBugAction->setIcon(QIcon::fromTheme(QStringLiteral("tools-report-bug")));
0186 
0187     // Help after it
0188     initHelpMenu();
0189 
0190     // Then a spacer so the search line-edit is kept separate
0191     spacerAction = new QWidgetAction(this);
0192     spacerAction->setDefaultWidget(spacerWidget);
0193     actionCollection()->addAction(QStringLiteral("spacer"), spacerAction);
0194     // Finally the search line-edit
0195     searchAction = new QWidgetAction(this);
0196     searchAction->setDefaultWidget(searchText);
0197     connect(searchAction, &QAction::triggered, searchText, QOverload<>::of(&KLineEdit::setFocus));
0198     actionCollection()->addAction(QStringLiteral("searchText"), searchAction);
0199     // Initialise the Window
0200     setupGUI(Save | Create, QString());
0201     menuBar()->hide();
0202 
0203     // Toolbar & Configuration
0204     helpActionMenu->setMenu(dynamic_cast<QMenu *>(factory()->container(QStringLiteral("help"), this)));
0205     toolBar()->setMovable(false); // We don't allow any changes
0206     changeToolBar(BaseMode::Search | BaseMode::Configure);
0207 }
0208 
0209 void SettingsBase::initHelpMenu()
0210 {
0211     helpActionMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("help-contents")), i18nd("systemsettings", "Help"), this);
0212     helpActionMenu->setPopupMode(QToolButton::InstantPopup);
0213     actionCollection()->addAction(QStringLiteral("help_toolbar_menu"), helpActionMenu);
0214     // Add the custom actions
0215     aboutViewAction = actionCollection()->addAction(KStandardAction::AboutApp, QStringLiteral("help_about_view"), this, &SettingsBase::about);
0216 }
0217 
0218 void SettingsBase::initMenuList(MenuItem *parent)
0219 {
0220     // look for any categories inside this level, and recurse into them
0221     for (const QString &category : qAsConst(categories)) {
0222         const KDesktopFile file(category);
0223         const KConfigGroup entry = file.desktopGroup();
0224         QString parentCategory;
0225         QString parentCategory2;
0226         if (m_mode == BaseMode::InfoCenter) {
0227             parentCategory = entry.readEntry("X-KDE-KInfoCenter-Parent-Category");
0228         } else {
0229             parentCategory = entry.readEntry("X-KDE-System-Settings-Parent-Category");
0230             parentCategory2 = entry.readEntry("X-KDE-System-Settings-Parent-Category-V2");
0231         }
0232 
0233         if (parentCategory == parent->category() ||
0234             // V2 entries must not be empty if they want to become a proper category.
0235             (!parentCategory2.isEmpty() && parentCategory2 == parent->category())) {
0236             MenuItem *menuItem = new MenuItem(true, parent);
0237             menuItem->setCategoryConfig(file);
0238             if (entry.readEntry("X-KDE-System-Settings-Category") == QLatin1String("lost-and-found")) {
0239                 lostFound = menuItem;
0240                 continue;
0241             }
0242             initMenuList(menuItem);
0243         }
0244     }
0245 
0246     // scan for any modules at this level and add them
0247     for (const auto &metaData : qAsConst(pluginModules)) {
0248         QString category;
0249         QString categoryv2;
0250         if (m_mode == BaseMode::InfoCenter) {
0251             category = metaData.value(QStringLiteral("X-KDE-KInfoCenter-Category"));
0252         } else {
0253             category = metaData.value(QStringLiteral("X-KDE-System-Settings-Parent-Category"));
0254             categoryv2 = metaData.value(QStringLiteral("X-KDE-System-Settings-Parent-Category-V2"));
0255         }
0256         const QString parentCategoryKcm = parent->systemsettingsCategoryModule();
0257         bool isCategoryOwner = false;
0258 
0259         if (!parentCategoryKcm.isEmpty() && parentCategoryKcm == metaData.pluginId()) {
0260             parent->setMetaData(metaData);
0261             isCategoryOwner = true;
0262         }
0263 
0264         if (!parent->category().isEmpty() && (category == parent->category() || categoryv2 == parent->category())) {
0265             if (!metaData.isHidden()) {
0266                 // Add the module info to the menu
0267                 MenuItem *infoItem = new MenuItem(false, parent);
0268                 infoItem->setMetaData(metaData);
0269                 infoItem->setCategoryOwner(isCategoryOwner);
0270 
0271                 if (m_mode == BaseMode::InfoCenter && metaData.pluginId() == QStringLiteral("kcm_about-distro")) {
0272                     homeModule = infoItem;
0273                 } else if (m_mode == BaseMode::SystemSettings && metaData.pluginId() == QStringLiteral("kcm_landingpage")) {
0274                     homeModule = infoItem;
0275                 }
0276             }
0277         }
0278     }
0279 
0280     parent->sortChildrenByWeight();
0281 }
0282 
0283 BaseMode *SettingsBase::loadCurrentView()
0284 {
0285     const QString viewToUse = m_mode == BaseMode::InfoCenter ? QStringLiteral("systemsettings_sidebar_mode") : BaseConfig::activeView();
0286 
0287     const auto pluginIt = std::find_if(m_plugins.cbegin(), m_plugins.cend(), [&viewToUse](const KPluginMetaData &plugin) {
0288         return viewToUse.contains(plugin.pluginId());
0289     });
0290 
0291     if (pluginIt == m_plugins.cend()) {
0292         return nullptr;
0293     }
0294 
0295     const auto controllerResult = KPluginFactory::instantiatePlugin<BaseMode>(*pluginIt, this, {m_mode, m_startupModule, m_startupModuleArgs});
0296     if (!controllerResult) {
0297         qCWarning(SYSTEMSETTINGS_APP_LOG) << "Error loading plugin" << controllerResult.errorText;
0298         return nullptr;
0299     }
0300 
0301     const auto controller = controllerResult.plugin;
0302     m_loadedViews.insert(viewToUse, controller);
0303     controller->init(*pluginIt);
0304     connect(controller, &BaseMode::changeToolBarItems, this, &SettingsBase::changeToolBar);
0305     connect(controller, &BaseMode::actionsChanged, this, &SettingsBase::updateViewActions);
0306     connect(searchText, &KLineEdit::textChanged, controller, &BaseMode::searchChanged);
0307     connect(controller, &BaseMode::viewChanged, this, &SettingsBase::viewChange);
0308 
0309     return controller;
0310 }
0311 
0312 bool SettingsBase::queryClose()
0313 {
0314     bool changes = true;
0315     if (activeView) {
0316         activeView->saveState();
0317         changes = activeView->moduleView()->resolveChanges();
0318     }
0319     BaseConfig::self()->save();
0320     return changes;
0321 }
0322 
0323 void SettingsBase::setStartupModule(const QString &startupModule)
0324 {
0325     m_startupModule = startupModule;
0326 
0327     if (activeView) {
0328         activeView->setStartupModule(startupModule);
0329     }
0330 }
0331 
0332 void SettingsBase::setStartupModuleArgs(const QStringList &startupModuleArgs)
0333 {
0334     m_startupModuleArgs = startupModuleArgs;
0335 
0336     if (activeView) {
0337         activeView->setStartupModuleArgs(startupModuleArgs);
0338     }
0339 }
0340 
0341 void SettingsBase::reloadStartupModule()
0342 {
0343     if (activeView) {
0344         activeView->reloadStartupModule();
0345     }
0346 }
0347 
0348 void SettingsBase::about()
0349 {
0350     delete aboutDialog;
0351     aboutDialog = nullptr;
0352 
0353     const KAboutData *about = nullptr;
0354     if (sender() == aboutViewAction) {
0355         about = activeView->aboutData();
0356     }
0357 
0358     if (about) {
0359         aboutDialog = new KAboutApplicationDialog(*about, nullptr);
0360         aboutDialog->show();
0361     }
0362 }
0363 
0364 void SettingsBase::changePlugin()
0365 {
0366     if (m_plugins.empty()) { // We should ensure we have a plugin available to choose
0367         KMessageBox::error(this,
0368                            i18nd("systemsettings", "System Settings was unable to find any views, and hence has nothing to display."),
0369                            i18nd("systemsettings", "No views found"));
0370         close();
0371         return; // Halt now!
0372     }
0373 
0374     // Don't let the user wait for nothing until the QML component is loaded.
0375     show();
0376 
0377     if (activeView) {
0378         activeView->saveState();
0379         activeView->leaveModuleView();
0380     }
0381 
0382     const QString viewToUse = m_mode == BaseMode::InfoCenter ? QStringLiteral("systemsettings_sidebar_mode") : BaseConfig::activeView();
0383     const auto it = m_loadedViews.constFind(viewToUse);
0384     if (it != m_loadedViews.cend()) {
0385         // First the configuration entry
0386         activeView = *it;
0387     } else if (auto *view = loadCurrentView()) {
0388         activeView = view;
0389     } else if (!m_loadedViews.empty()) { // Otherwise we activate the failsafe
0390         qCWarning(SYSTEMSETTINGS_APP_LOG) << "System Settings was unable to load" << viewToUse;
0391         activeView = m_loadedViews.cbegin().value();
0392     } else {
0393         // Current view is missing on startup, try to load alternate view.
0394         qCWarning(SYSTEMSETTINGS_APP_LOG) << "System Settings was unable to load" << viewToUse;
0395         if (viewToUse == QStringLiteral("systemsettings_icon_mode")) {
0396             BaseConfig::setActiveView(QStringLiteral("systemsettings_sidebar_mode"));
0397         } else if (m_mode != BaseMode::InfoCenter) {
0398             BaseConfig::setActiveView(QStringLiteral("systemsettings_icon_mode"));
0399         }
0400 
0401         if (auto *view = loadCurrentView()) {
0402             activeView = view;
0403             activeView->saveState();
0404         } else {
0405             qCWarning(SYSTEMSETTINGS_APP_LOG) << "System Settings was unable to load any views, and hence has nothing to display.";
0406             close();
0407             return; // Halt now!
0408         }
0409     }
0410 
0411     if (stackedWidget->indexOf(activeView->mainWidget()) == -1) {
0412         stackedWidget->addWidget(activeView->mainWidget());
0413     }
0414 
0415     // Handle the tooltips
0416     qDeleteAll(tooltipManagers);
0417     tooltipManagers.clear();
0418     const QList<QAbstractItemView *> theViews = activeView->views();
0419     for (QAbstractItemView *view : theViews) {
0420         tooltipManagers << new ToolTipManager(view);
0421     }
0422 
0423     if (highlightChangesAction) {
0424         highlightChangesAction->setChecked(activeView->defaultsIndicatorsVisible());
0425     }
0426 
0427     changeAboutMenu(activeView->aboutData(), aboutViewAction, i18nd("systemsettings", "About Active View"));
0428     viewChange(false);
0429 
0430     stackedWidget->setCurrentWidget(activeView->mainWidget());
0431     updateViewActions();
0432 
0433     activeView->giveFocus();
0434 
0435     // Update visibility of the "report a bug on this page" and "report bug in general"
0436     // actions based on whether the current page has a bug report URL set
0437     auto reportGeneralBugAction = actionCollection()->action(QStringLiteral("help_report_bug"));
0438     reportGeneralBugAction->setVisible(false);
0439     auto moduleView = activeView->moduleView();
0440     connect(moduleView, &ModuleView::moduleChanged, this, [=] {
0441         reportPageSpecificBugAction->setVisible(!moduleView->activeModuleMetadata().bugReportUrl().isEmpty());
0442         reportGeneralBugAction->setVisible(!reportPageSpecificBugAction->isVisible());
0443     });
0444 }
0445 
0446 void SettingsBase::viewChange(bool state)
0447 {
0448     setCaption(activeView->moduleView()->activeModuleName(), state);
0449 }
0450 
0451 void SettingsBase::updateViewActions()
0452 {
0453     guiFactory()->unplugActionList(this, QStringLiteral("viewActions"));
0454     guiFactory()->plugActionList(this, QStringLiteral("viewActions"), activeView->actionsList());
0455 }
0456 
0457 void SettingsBase::changeToolBar(BaseMode::ToolBarItems toolbar)
0458 {
0459     if (sender() != activeView) {
0460         return;
0461     }
0462     guiFactory()->unplugActionList(this, QStringLiteral("configure"));
0463     guiFactory()->unplugActionList(this, QStringLiteral("search"));
0464     guiFactory()->unplugActionList(this, QStringLiteral("quit"));
0465     if (BaseMode::Search & toolbar) {
0466         QList<QAction *> searchBarActions;
0467         searchBarActions << spacerAction << searchAction;
0468         guiFactory()->plugActionList(this, QStringLiteral("search"), searchBarActions);
0469         actionCollection()->setDefaultShortcut(searchAction, QKeySequence(Qt::CTRL | Qt::Key_F));
0470     }
0471     if ((BaseMode::Configure & toolbar) && switchToSidebarAction) {
0472         QList<QAction *> configureBarActions;
0473         configureBarActions << switchToSidebarAction;
0474         guiFactory()->plugActionList(this, QStringLiteral("configure"), configureBarActions);
0475     }
0476     if (BaseMode::Quit & toolbar) {
0477         QList<QAction *> quitBarActions;
0478         quitBarActions << quitAction;
0479         guiFactory()->plugActionList(this, QStringLiteral("quit"), quitBarActions);
0480     }
0481     if (BaseMode::NoItems & toolbar) {
0482         // Remove search shortcut when there's no toolbar so it doesn't
0483         // interfere with the built-in shortcut for the search field in the QML
0484         // sidebar view
0485         actionCollection()->setDefaultShortcut(searchAction, QKeySequence());
0486     }
0487 
0488     toolBar()->setVisible(toolbar != BaseMode::NoItems || (activeView && activeView->actionsList().count() > 0));
0489 }
0490 
0491 void SettingsBase::changeAboutMenu(const KAboutData *menuAbout, QAction *menuItem, const QString &fallback)
0492 {
0493     if (!menuItem) {
0494         return;
0495     }
0496 
0497     if (menuAbout) {
0498         menuItem->setText(i18nd("systemsettings", "About %1", menuAbout->displayName()));
0499         menuItem->setIcon(QGuiApplication::windowIcon());
0500         menuItem->setEnabled(true);
0501     } else {
0502         menuItem->setText(fallback);
0503         menuItem->setIcon(QGuiApplication::windowIcon());
0504         menuItem->setEnabled(false);
0505     }
0506 }
0507 
0508 void SettingsBase::slotGeometryChanged()
0509 {
0510     setMinimumSize(SettingsBase::sizeHint());
0511 }