File indexing completed on 2024-04-21 15:05:55

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 Reginald Stadlbauer <reggie@kde.org>
0004     SPDX-FileCopyrightText: 1997 Stephan Kulow <coolo@kde.org>
0005     SPDX-FileCopyrightText: 1997-2000 Sven Radej <radej@kde.org>
0006     SPDX-FileCopyrightText: 1997-2000 Matthias Ettrich <ettrich@kde.org>
0007     SPDX-FileCopyrightText: 1999 Chris Schlaeger <cs@kde.org>
0008     SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
0009     SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>
0010 
0011     SPDX-License-Identifier: LGPL-2.0-only
0012 */
0013 
0014 #include "kxmlguiwindow.h"
0015 #include "debug.h"
0016 
0017 #include "kactioncollection.h"
0018 #include "kmainwindow_p.h"
0019 #include <KMessageBox>
0020 #ifdef QT_DBUS_LIB
0021 #include "kmainwindowiface_p.h"
0022 #endif
0023 #include "kedittoolbar.h"
0024 #include "khelpmenu.h"
0025 #include "ktoolbar.h"
0026 #include "ktoolbarhandler_p.h"
0027 #include "kxmlguifactory.h"
0028 
0029 #ifdef QT_DBUS_LIB
0030 #include <QDBusConnection>
0031 #endif
0032 #include <QDomDocument>
0033 #include <QEvent>
0034 #include <QList>
0035 #include <QMenuBar>
0036 #include <QStatusBar>
0037 #include <QWidget>
0038 
0039 #include <KAboutData>
0040 #include <KCommandBar>
0041 #include <KConfig>
0042 #include <KConfigGroup>
0043 #include <KLocalizedString>
0044 #include <KSharedConfig>
0045 #include <KStandardAction>
0046 #include <KToggleAction>
0047 
0048 #include <cctype>
0049 #include <cstdlib>
0050 
0051 /**
0052  * A helper function that takes a list of KActionCollection* and converts it
0053  * to KCommandBar::ActionGroup
0054  */
0055 static QVector<KCommandBar::ActionGroup> actionCollectionToActionGroup(const std::vector<KActionCollection *> &actionCollections)
0056 {
0057     using ActionGroup = KCommandBar::ActionGroup;
0058 
0059     QVector<ActionGroup> actionList;
0060     actionList.reserve(actionCollections.size());
0061 
0062     for (const auto collection : actionCollections) {
0063         const QList<QAction *> collectionActions = collection->actions();
0064         const QString componentName = collection->componentDisplayName();
0065 
0066         ActionGroup ag;
0067         ag.name = componentName;
0068         ag.actions.reserve(collection->count());
0069         for (const auto action : collectionActions) {
0070             /**
0071              * If this action is a menu, fetch all its child actions
0072              * and skip the menu action itself
0073              */
0074             if (QMenu *menu = action->menu()) {
0075                 const QList<QAction *> menuActions = menu->actions();
0076 
0077                 ActionGroup menuActionGroup;
0078                 menuActionGroup.name = KLocalizedString::removeAcceleratorMarker(action->text());
0079                 menuActionGroup.actions.reserve(menuActions.size());
0080                 for (const auto mAct : menuActions) {
0081                     if (mAct) {
0082                         menuActionGroup.actions.append(mAct);
0083                     }
0084                 }
0085 
0086                 /**
0087                  * If there were no actions in the menu, we
0088                  * add the menu to the list instead because it could
0089                  * be that the actions are created on demand i.e., aboutToShow()
0090                  */
0091                 if (!menuActions.isEmpty()) {
0092                     actionList.append(menuActionGroup);
0093                     continue;
0094                 }
0095             }
0096 
0097             if (action && !action->text().isEmpty()) {
0098                 ag.actions.append(action);
0099             }
0100         }
0101         actionList.append(ag);
0102     }
0103     return actionList;
0104 }
0105 
0106 static void getActionCollections(KXMLGUIClient *client, std::vector<KActionCollection *> &actionCollections)
0107 {
0108     if (!client) {
0109         return;
0110     }
0111 
0112     auto actionCollection = client->actionCollection();
0113     if (actionCollection && !actionCollection->isEmpty()) {
0114         actionCollections.push_back(client->actionCollection());
0115     }
0116 
0117     const QList<KXMLGUIClient *> childClients = client->childClients();
0118     for (auto child : childClients) {
0119         getActionCollections(child, actionCollections);
0120     }
0121 }
0122 
0123 class KXmlGuiWindowPrivate : public KMainWindowPrivate
0124 {
0125 public:
0126     void slotFactoryMakingChanges(bool b)
0127     {
0128         // While the GUI factory is adding/removing clients,
0129         // don't let KMainWindow think those are changes made by the user
0130         // #105525
0131         letDirtySettings = !b;
0132     }
0133 
0134     bool commandBarEnabled = true;
0135     // Last executed actions in command bar
0136     QVector<QString> lastExecutedActions;
0137 
0138     bool showHelpMenu : 1;
0139     QSize defaultSize;
0140 
0141     KDEPrivate::ToolBarHandler *toolBarHandler;
0142     KToggleAction *showStatusBarAction;
0143     QPointer<KEditToolBar> toolBarEditor;
0144     KXMLGUIFactory *factory;
0145 };
0146 
0147 KXmlGuiWindow::KXmlGuiWindow(QWidget *parent, Qt::WindowFlags flags)
0148     : KMainWindow(*new KXmlGuiWindowPrivate, parent, flags)
0149     , KXMLGUIBuilder(this)
0150 {
0151     Q_D(KXmlGuiWindow);
0152     d->showHelpMenu = true;
0153     d->toolBarHandler = nullptr;
0154     d->showStatusBarAction = nullptr;
0155     d->factory = nullptr;
0156 #ifdef QT_DBUS_LIB
0157     new KMainWindowInterface(this);
0158 #endif
0159 
0160     /*
0161      * Set up KCommandBar launcher action
0162      */
0163     auto a = actionCollection()->addAction(QStringLiteral("open_kcommand_bar"), this, [this] {
0164         /*
0165          * Do nothing when command bar is disabled
0166          */
0167         if (!isCommandBarEnabled()) {
0168             return;
0169         }
0170 
0171         auto ac = actionCollection();
0172         if (!ac) {
0173             return;
0174         }
0175 
0176         KCommandBar kc(this);
0177         std::vector<KActionCollection *> actionCollections;
0178         const auto clients = guiFactory()->clients();
0179         actionCollections.reserve(clients.size());
0180 
0181         // Grab action collections recursively
0182         for (const auto &client : clients) {
0183             getActionCollections(client, actionCollections);
0184         }
0185 
0186         kc.setActions(actionCollectionToActionGroup(actionCollections));
0187         kc.exec();
0188     });
0189     a->setIcon(QIcon::fromTheme(QStringLiteral("search")));
0190     a->setText(i18n("Find Action…"));
0191     actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_I));
0192 }
0193 
0194 QAction *KXmlGuiWindow::toolBarMenuAction()
0195 {
0196     Q_D(KXmlGuiWindow);
0197     if (!d->toolBarHandler) {
0198         return nullptr;
0199     }
0200 
0201     return d->toolBarHandler->toolBarMenuAction();
0202 }
0203 
0204 void KXmlGuiWindow::setupToolbarMenuActions()
0205 {
0206     Q_D(KXmlGuiWindow);
0207     if (d->toolBarHandler) {
0208         d->toolBarHandler->setupActions();
0209     }
0210 }
0211 
0212 KXmlGuiWindow::~KXmlGuiWindow()
0213 {
0214     Q_D(KXmlGuiWindow);
0215     delete d->factory;
0216 }
0217 
0218 bool KXmlGuiWindow::event(QEvent *ev)
0219 {
0220     bool ret = KMainWindow::event(ev);
0221     if (ev->type() == QEvent::Polish) {
0222 #ifdef QT_DBUS_LIB
0223         /* clang-format off */
0224         constexpr auto opts = QDBusConnection::ExportScriptableSlots
0225                               | QDBusConnection::ExportScriptableProperties
0226                               | QDBusConnection::ExportNonScriptableSlots
0227                               | QDBusConnection::ExportNonScriptableProperties
0228                               | QDBusConnection::ExportChildObjects;
0229         /* clang-format on */
0230         QDBusConnection::sessionBus().registerObject(dbusName() + QLatin1String("/actions"), actionCollection(), opts);
0231 #endif
0232     }
0233     return ret;
0234 }
0235 
0236 void KXmlGuiWindow::setHelpMenuEnabled(bool showHelpMenu)
0237 {
0238     Q_D(KXmlGuiWindow);
0239     d->showHelpMenu = showHelpMenu;
0240 }
0241 
0242 bool KXmlGuiWindow::isHelpMenuEnabled() const
0243 {
0244     Q_D(const KXmlGuiWindow);
0245     return d->showHelpMenu;
0246 }
0247 
0248 KXMLGUIFactory *KXmlGuiWindow::guiFactory()
0249 {
0250     Q_D(KXmlGuiWindow);
0251     if (!d->factory) {
0252         d->factory = new KXMLGUIFactory(this, this);
0253         connect(d->factory, &KXMLGUIFactory::makingChanges, this, [d](bool state) {
0254             d->slotFactoryMakingChanges(state);
0255         });
0256     }
0257     return d->factory;
0258 }
0259 
0260 void KXmlGuiWindow::configureToolbars()
0261 {
0262     Q_D(KXmlGuiWindow);
0263     KConfigGroup cg(KSharedConfig::openConfig(), "");
0264     saveMainWindowSettings(cg);
0265     if (!d->toolBarEditor) {
0266         d->toolBarEditor = new KEditToolBar(guiFactory(), this);
0267         d->toolBarEditor->setAttribute(Qt::WA_DeleteOnClose);
0268         connect(d->toolBarEditor, &KEditToolBar::newToolBarConfig, this, &KXmlGuiWindow::saveNewToolbarConfig);
0269     }
0270     d->toolBarEditor->show();
0271 }
0272 
0273 void KXmlGuiWindow::saveNewToolbarConfig()
0274 {
0275     // createGUI(xmlFile()); // this loses any plugged-in guiclients, so we use remove+add instead.
0276 
0277     guiFactory()->removeClient(this);
0278     guiFactory()->addClient(this);
0279 
0280     KConfigGroup cg(KSharedConfig::openConfig(), "");
0281     applyMainWindowSettings(cg);
0282 }
0283 
0284 void KXmlGuiWindow::setupGUI(StandardWindowOptions options, const QString &xmlfile)
0285 {
0286     setupGUI(QSize(), options, xmlfile);
0287 }
0288 
0289 void KXmlGuiWindow::setupGUI(const QSize &defaultSize, StandardWindowOptions options, const QString &xmlfile)
0290 {
0291     Q_D(KXmlGuiWindow);
0292 
0293     if (options & Keys) {
0294         KStandardAction::keyBindings(guiFactory(), &KXMLGUIFactory::showConfigureShortcutsDialog, actionCollection());
0295     }
0296 
0297     if ((options & StatusBar) && statusBar()) {
0298         createStandardStatusBarAction();
0299     }
0300 
0301     if (options & ToolBar) {
0302         setStandardToolBarMenuEnabled(true);
0303         KStandardAction::configureToolbars(this, &KXmlGuiWindow::configureToolbars, actionCollection());
0304     }
0305 
0306     d->defaultSize = defaultSize;
0307 
0308     if (options & Create) {
0309         createGUI(xmlfile);
0310     }
0311 
0312     if (d->defaultSize.isValid()) {
0313         resize(d->defaultSize);
0314     } else if (isHidden()) {
0315         adjustSize();
0316     }
0317 
0318     if (options & Save) {
0319         const KConfigGroup cg(autoSaveConfigGroup());
0320         if (cg.isValid()) {
0321             setAutoSaveSettings(cg);
0322         } else {
0323             setAutoSaveSettings();
0324         }
0325     }
0326 }
0327 void KXmlGuiWindow::createGUI(const QString &xmlfile)
0328 {
0329     Q_D(KXmlGuiWindow);
0330     // disabling the updates prevents unnecessary redraws
0331     // setUpdatesEnabled( false );
0332 
0333     // just in case we are rebuilding, let's remove our old client
0334     guiFactory()->removeClient(this);
0335 
0336     // make sure to have an empty GUI
0337     QMenuBar *mb = menuBar();
0338     if (mb) {
0339         mb->clear();
0340     }
0341 
0342     qDeleteAll(toolBars()); // delete all toolbars
0343 
0344     // don't build a help menu unless the user ask for it
0345     if (d->showHelpMenu) {
0346         delete d->helpMenu;
0347         // we always want a help menu
0348         d->helpMenu = new KHelpMenu(this, KAboutData::applicationData(), true);
0349 
0350         KActionCollection *actions = actionCollection();
0351         QAction *helpContentsAction = d->helpMenu->action(KHelpMenu::menuHelpContents);
0352         QAction *whatsThisAction = d->helpMenu->action(KHelpMenu::menuWhatsThis);
0353         QAction *reportBugAction = d->helpMenu->action(KHelpMenu::menuReportBug);
0354         QAction *switchLanguageAction = d->helpMenu->action(KHelpMenu::menuSwitchLanguage);
0355         QAction *aboutAppAction = d->helpMenu->action(KHelpMenu::menuAboutApp);
0356         QAction *aboutKdeAction = d->helpMenu->action(KHelpMenu::menuAboutKDE);
0357         QAction *donateAction = d->helpMenu->action(KHelpMenu::menuDonate);
0358 
0359         if (helpContentsAction) {
0360             actions->addAction(helpContentsAction->objectName(), helpContentsAction);
0361         }
0362         if (whatsThisAction) {
0363             actions->addAction(whatsThisAction->objectName(), whatsThisAction);
0364         }
0365         if (reportBugAction) {
0366             actions->addAction(reportBugAction->objectName(), reportBugAction);
0367         }
0368         if (switchLanguageAction) {
0369             actions->addAction(switchLanguageAction->objectName(), switchLanguageAction);
0370         }
0371         if (aboutAppAction) {
0372             actions->addAction(aboutAppAction->objectName(), aboutAppAction);
0373         }
0374         if (aboutKdeAction) {
0375             actions->addAction(aboutKdeAction->objectName(), aboutKdeAction);
0376         }
0377         if (donateAction) {
0378             actions->addAction(donateAction->objectName(), donateAction);
0379         }
0380     }
0381 
0382     const QString windowXmlFile = xmlfile.isNull() ? componentName() + QLatin1String("ui.rc") : xmlfile;
0383 
0384     // Help beginners who call setXMLFile and then setupGUI...
0385     if (!xmlFile().isEmpty() && xmlFile() != windowXmlFile) {
0386         qCWarning(DEBUG_KXMLGUI) << "You called setXMLFile(" << xmlFile() << ") and then createGUI or setupGUI,"
0387                                  << "which also calls setXMLFile and will overwrite the file you have previously set.\n"
0388                                  << "You should call createGUI(" << xmlFile() << ") or setupGUI(<options>," << xmlFile() << ") instead.";
0389     }
0390 
0391     // we always want to load in our global standards file
0392     loadStandardsXmlFile();
0393 
0394     // now, merge in our local xml file.
0395     setXMLFile(windowXmlFile, true);
0396 
0397     // make sure we don't have any state saved already
0398     setXMLGUIBuildDocument(QDomDocument());
0399 
0400     // do the actual GUI building
0401     guiFactory()->reset();
0402     guiFactory()->addClient(this);
0403 
0404     checkAmbiguousShortcuts();
0405 
0406     //  setUpdatesEnabled( true );
0407 }
0408 
0409 void KXmlGuiWindow::slotStateChanged(const QString &newstate)
0410 {
0411     stateChanged(newstate, KXMLGUIClient::StateNoReverse);
0412 }
0413 
0414 void KXmlGuiWindow::slotStateChanged(const QString &newstate, bool reverse)
0415 {
0416     stateChanged(newstate, reverse ? KXMLGUIClient::StateReverse : KXMLGUIClient::StateNoReverse);
0417 }
0418 
0419 void KXmlGuiWindow::setStandardToolBarMenuEnabled(bool showToolBarMenu)
0420 {
0421     Q_D(KXmlGuiWindow);
0422     if (showToolBarMenu) {
0423         if (d->toolBarHandler) {
0424             return;
0425         }
0426 
0427         d->toolBarHandler = new KDEPrivate::ToolBarHandler(this);
0428 
0429         if (factory()) {
0430             factory()->addClient(d->toolBarHandler);
0431         }
0432     } else {
0433         if (!d->toolBarHandler) {
0434             return;
0435         }
0436 
0437         if (factory()) {
0438             factory()->removeClient(d->toolBarHandler);
0439         }
0440 
0441         delete d->toolBarHandler;
0442         d->toolBarHandler = nullptr;
0443     }
0444 }
0445 
0446 bool KXmlGuiWindow::isStandardToolBarMenuEnabled() const
0447 {
0448     Q_D(const KXmlGuiWindow);
0449     return (d->toolBarHandler);
0450 }
0451 
0452 void KXmlGuiWindow::createStandardStatusBarAction()
0453 {
0454     Q_D(KXmlGuiWindow);
0455     if (!d->showStatusBarAction) {
0456         d->showStatusBarAction = KStandardAction::showStatusbar(this, &KMainWindow::setSettingsDirty, actionCollection());
0457         QStatusBar *sb = statusBar(); // Creates statusbar if it doesn't exist already.
0458         connect(d->showStatusBarAction, &QAction::toggled, sb, &QWidget::setVisible);
0459         d->showStatusBarAction->setChecked(sb->isHidden());
0460     } else {
0461         // If the language has changed, we'll need to grab the new text and whatsThis
0462         QAction *tmpStatusBar = KStandardAction::showStatusbar(nullptr, nullptr, nullptr);
0463         d->showStatusBarAction->setText(tmpStatusBar->text());
0464         d->showStatusBarAction->setWhatsThis(tmpStatusBar->whatsThis());
0465         delete tmpStatusBar;
0466     }
0467 }
0468 
0469 void KXmlGuiWindow::finalizeGUI(bool /*force*/)
0470 {
0471     // FIXME: this really needs to be removed with a code more like the one we had on KDE3.
0472     //        what we need to do here is to position correctly toolbars so they don't overlap.
0473     //        Also, take in count plugins could provide their own toolbars and those also need to
0474     //        be restored.
0475     if (autoSaveSettings() && autoSaveConfigGroup().isValid()) {
0476         applyMainWindowSettings(autoSaveConfigGroup());
0477     }
0478 }
0479 
0480 void KXmlGuiWindow::applyMainWindowSettings(const KConfigGroup &config)
0481 {
0482     Q_D(KXmlGuiWindow);
0483     KMainWindow::applyMainWindowSettings(config);
0484     QStatusBar *sb = findChild<QStatusBar *>();
0485     if (sb && d->showStatusBarAction) {
0486         d->showStatusBarAction->setChecked(!sb->isHidden());
0487     }
0488 }
0489 
0490 // TODO KF6: change it to "using KXMLGUIBuilder::finalizeGUI;" in the header
0491 // and remove the reimplementation
0492 void KXmlGuiWindow::finalizeGUI(KXMLGUIClient *client)
0493 {
0494     KXMLGUIBuilder::finalizeGUI(client);
0495 }
0496 
0497 void KXmlGuiWindow::checkAmbiguousShortcuts()
0498 {
0499     QMap<QString, QAction *> shortcuts;
0500     QAction *editCutAction = actionCollection()->action(QStringLiteral("edit_cut"));
0501     QAction *deleteFileAction = actionCollection()->action(QStringLiteral("deletefile"));
0502     const auto actions = actionCollection()->actions();
0503     for (QAction *action : actions) {
0504         if (action->isEnabled()) {
0505             const auto actionShortcuts = action->shortcuts();
0506             for (const QKeySequence &shortcut : actionShortcuts) {
0507                 if (shortcut.isEmpty()) {
0508                     continue;
0509                 }
0510                 const QString portableShortcutText = shortcut.toString();
0511                 const QAction *existingShortcutAction = shortcuts.value(portableShortcutText);
0512                 if (existingShortcutAction) {
0513                     // If the shortcut is already in use we give a warning, so that hopefully the developer will find it
0514                     // There is one exception, if the conflicting shortcut is a non primary shortcut of "edit_cut"
0515                     // and "deleteFileAction" is the other action since Shift+Delete is used for both in our default code
0516                     bool showWarning = true;
0517                     if ((action == editCutAction && existingShortcutAction == deleteFileAction)
0518                         || (action == deleteFileAction && existingShortcutAction == editCutAction)) {
0519                         QList<QKeySequence> editCutActionShortcuts = editCutAction->shortcuts();
0520                         if (editCutActionShortcuts.indexOf(shortcut) > 0) // alternate shortcut
0521                         {
0522                             editCutActionShortcuts.removeAll(shortcut);
0523                             editCutAction->setShortcuts(editCutActionShortcuts);
0524 
0525                             showWarning = false;
0526                         }
0527                     }
0528 
0529                     if (showWarning) {
0530                         const QString actionName = KLocalizedString::removeAcceleratorMarker(action->text());
0531                         const QString existingShortcutActionName = KLocalizedString::removeAcceleratorMarker(existingShortcutAction->text());
0532                         QString dontShowAgainString = existingShortcutActionName + actionName + shortcut.toString();
0533                         dontShowAgainString.remove(QLatin1Char('\\'));
0534                         KMessageBox::information(this,
0535                                                  i18n("There are two actions (%1, %2) that want to use the same shortcut (%3). This is most probably a bug. "
0536                                                       "Please report it in <a href='https://bugs.kde.org'>bugs.kde.org</a>",
0537                                                       existingShortcutActionName,
0538                                                       actionName,
0539                                                       shortcut.toString(QKeySequence::NativeText)),
0540                                                  i18n("Ambiguous Shortcuts"),
0541                                                  dontShowAgainString,
0542                                                  KMessageBox::Notify | KMessageBox::AllowLink);
0543                     }
0544                 } else {
0545                     shortcuts.insert(portableShortcutText, action);
0546                 }
0547             }
0548         }
0549     }
0550 }
0551 
0552 void KXmlGuiWindow::setCommandBarEnabled(bool showCommandBar)
0553 {
0554     /**
0555      * Unset the shortcut
0556      */
0557     auto cmdBarAction = actionCollection()->action(QStringLiteral("open_kcommand_bar"));
0558     if (showCommandBar) {
0559         actionCollection()->setDefaultShortcut(cmdBarAction, Qt::CTRL | Qt::ALT | Qt::Key_I);
0560     } else {
0561         actionCollection()->setDefaultShortcut(cmdBarAction, {});
0562     }
0563 
0564     Q_D(KXmlGuiWindow);
0565     d->commandBarEnabled = showCommandBar;
0566 }
0567 
0568 bool KXmlGuiWindow::isCommandBarEnabled() const
0569 {
0570     Q_D(const KXmlGuiWindow);
0571     return d->commandBarEnabled;
0572 }
0573 
0574 #include "moc_kxmlguiwindow.cpp"