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 }