File indexing completed on 2024-04-28 07:48:45

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