1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Reginald Stadlbauer <reggie@kde.org>
4 SPDX-FileCopyrightText: 1997 Stephan Kulow <coolo@kde.org>
5 SPDX-FileCopyrightText: 1997-2000 Sven Radej <radej@kde.org>
6 SPDX-FileCopyrightText: 1997-2000 Matthias Ettrich <ettrich@kde.org>
7 SPDX-FileCopyrightText: 1999 Chris Schlaeger <cs@kde.org>
8 SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
9 SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>
10
11 SPDX-License-Identifier: LGPL-2.0-only
12*/
13
14#include "kxmlguiwindow.h"
15#include "debug.h"
16
17#include "kactioncollection.h"
18#include "kmainwindow_p.h"
19#include <KMessageBox>
20#include <kcommandbar.h>
21#ifdef WITH_QTDBUS
22#include "kmainwindowiface_p.h"
23#endif
24#include "kedittoolbar.h"
25#include "khelpmenu.h"
26#include "ktoolbar.h"
27#include "ktoolbarhandler_p.h"
28#include "kxmlguifactory.h"
29
30#ifdef WITH_QTDBUS
31#include <QDBusConnection>
32#endif
33#include <QDomDocument>
34#include <QEvent>
35#include <QList>
36#include <QMenuBar>
37#include <QStatusBar>
38#include <QWidget>
39
40#include <KAboutData>
41#include <KCommandBar>
42#include <KConfig>
43#include <KConfigGroup>
44#include <KLocalizedString>
45#include <KSharedConfig>
46#include <KStandardAction>
47#include <KToggleAction>
48
49#include <cctype>
50#include <cstdlib>
51
52/**
53 * A helper function that takes a list of KActionCollection* and converts it
54 * to KCommandBar::ActionGroup
55 */
56static QList<KCommandBar::ActionGroup> actionCollectionToActionGroup(const std::vector<KActionCollection *> &actionCollections)
57{
58 using ActionGroup = KCommandBar::ActionGroup;
59
60 QList<ActionGroup> actionList;
61 actionList.reserve(asize: actionCollections.size());
62
63 for (const auto collection : actionCollections) {
64 const QList<QAction *> collectionActions = collection->actions();
65 const QString componentName = collection->componentDisplayName();
66
67 ActionGroup ag;
68 ag.name = componentName;
69 ag.actions.reserve(asize: collection->count());
70 for (const auto action : collectionActions) {
71 /**
72 * If this action is a menu, fetch all its child actions
73 * and skip the menu action itself
74 */
75 if (QMenu *menu = action->menu()) {
76 const QList<QAction *> menuActions = menu->actions();
77
78 ActionGroup menuActionGroup;
79 menuActionGroup.name = KLocalizedString::removeAcceleratorMarker(label: action->text());
80 menuActionGroup.actions.reserve(asize: menuActions.size());
81 for (const auto mAct : menuActions) {
82 if (mAct) {
83 menuActionGroup.actions.append(t: mAct);
84 }
85 }
86
87 /**
88 * If there were no actions in the menu, we
89 * add the menu to the list instead because it could
90 * be that the actions are created on demand i.e., aboutToShow()
91 */
92 if (!menuActions.isEmpty()) {
93 actionList.append(t: menuActionGroup);
94 continue;
95 }
96 }
97
98 if (action && !action->text().isEmpty()) {
99 ag.actions.append(t: action);
100 }
101 }
102 actionList.append(t: ag);
103 }
104 return actionList;
105}
106
107static void getActionCollections(KXMLGUIClient *client, std::vector<KActionCollection *> &actionCollections)
108{
109 if (!client) {
110 return;
111 }
112
113 auto actionCollection = client->actionCollection();
114 if (actionCollection && !actionCollection->isEmpty()) {
115 actionCollections.push_back(x: client->actionCollection());
116 }
117
118 const QList<KXMLGUIClient *> childClients = client->childClients();
119 for (auto child : childClients) {
120 getActionCollections(client: child, actionCollections);
121 }
122}
123
124class KXmlGuiWindowPrivate : public KMainWindowPrivate
125{
126public:
127 void slotFactoryMakingChanges(bool b)
128 {
129 // While the GUI factory is adding/removing clients,
130 // don't let KMainWindow think those are changes made by the user
131 // #105525
132 letDirtySettings = !b;
133 }
134
135 bool commandBarEnabled = true;
136 // Last executed actions in command bar
137 QList<QString> lastExecutedActions;
138
139 bool showHelpMenu : 1;
140 QSize defaultSize;
141
142 KDEPrivate::ToolBarHandler *toolBarHandler;
143 KToggleAction *showStatusBarAction;
144 QPointer<KEditToolBar> toolBarEditor;
145 KXMLGUIFactory *factory;
146};
147
148KXmlGuiWindow::KXmlGuiWindow(QWidget *parent, Qt::WindowFlags flags)
149 : KMainWindow(*new KXmlGuiWindowPrivate, parent, flags)
150 , KXMLGUIBuilder(this)
151{
152 Q_D(KXmlGuiWindow);
153 d->showHelpMenu = true;
154 d->toolBarHandler = nullptr;
155 d->showStatusBarAction = nullptr;
156 d->factory = nullptr;
157#ifdef WITH_QTDBUS
158 new KMainWindowInterface(this);
159#endif
160
161 /*
162 * Set up KCommandBar launcher action
163 */
164 auto a = actionCollection()->addAction(QStringLiteral("open_kcommand_bar"), receiver: this, slot: [this] {
165 /*
166 * Do nothing when command bar is disabled
167 */
168 if (!isCommandBarEnabled()) {
169 return;
170 }
171
172 auto ac = actionCollection();
173 if (!ac) {
174 return;
175 }
176
177 auto kc = new KCommandBar(this);
178 std::vector<KActionCollection *> actionCollections;
179 const auto clients = guiFactory()->clients();
180 actionCollections.reserve(n: clients.size());
181
182 // Grab action collections recursively
183 for (const auto &client : clients) {
184 getActionCollections(client, actionCollections);
185 }
186
187 kc->setActions(actionCollectionToActionGroup(actionCollections));
188 kc->show();
189 });
190 a->setIcon(QIcon::fromTheme(QStringLiteral("search")));
191 a->setText(i18n("Find Action…"));
192 KActionCollection::setDefaultShortcut(action: a, shortcut: QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_I));
193}
194
195QAction *KXmlGuiWindow::toolBarMenuAction()
196{
197 Q_D(KXmlGuiWindow);
198 if (!d->toolBarHandler) {
199 return nullptr;
200 }
201
202 return d->toolBarHandler->toolBarMenuAction();
203}
204
205void KXmlGuiWindow::setupToolbarMenuActions()
206{
207 Q_D(KXmlGuiWindow);
208 if (d->toolBarHandler) {
209 d->toolBarHandler->setupActions();
210 }
211}
212
213KXmlGuiWindow::~KXmlGuiWindow()
214{
215 Q_D(KXmlGuiWindow);
216 delete d->factory;
217}
218
219bool KXmlGuiWindow::event(QEvent *ev)
220{
221 bool ret = KMainWindow::event(event: ev);
222 if (ev->type() == QEvent::Polish) {
223#ifdef WITH_QTDBUS
224 /* clang-format off */
225 constexpr auto opts = QDBusConnection::ExportScriptableSlots
226 | QDBusConnection::ExportScriptableProperties
227 | QDBusConnection::ExportNonScriptableSlots
228 | QDBusConnection::ExportNonScriptableProperties
229 | QDBusConnection::ExportChildObjects;
230 /* clang-format on */
231 QDBusConnection::sessionBus().registerObject(path: dbusName() + QLatin1String("/actions"), object: actionCollection(), options: opts);
232#endif
233 }
234 return ret;
235}
236
237void KXmlGuiWindow::setHelpMenuEnabled(bool showHelpMenu)
238{
239 Q_D(KXmlGuiWindow);
240 d->showHelpMenu = showHelpMenu;
241}
242
243bool KXmlGuiWindow::isHelpMenuEnabled() const
244{
245 Q_D(const KXmlGuiWindow);
246 return d->showHelpMenu;
247}
248
249KXMLGUIFactory *KXmlGuiWindow::guiFactory()
250{
251 Q_D(KXmlGuiWindow);
252 if (!d->factory) {
253 d->factory = new KXMLGUIFactory(this, this);
254 connect(sender: d->factory, signal: &KXMLGUIFactory::makingChanges, context: this, slot: [d](bool state) {
255 d->slotFactoryMakingChanges(b: state);
256 });
257 }
258 return d->factory;
259}
260
261void KXmlGuiWindow::configureToolbars()
262{
263 Q_D(KXmlGuiWindow);
264 KConfigGroup cg(KSharedConfig::openConfig(), QString());
265 saveMainWindowSettings(config&: cg);
266 if (!d->toolBarEditor) {
267 d->toolBarEditor = new KEditToolBar(guiFactory(), this);
268 d->toolBarEditor->setAttribute(Qt::WA_DeleteOnClose);
269 connect(sender: d->toolBarEditor, signal: &KEditToolBar::newToolBarConfig, context: this, slot: &KXmlGuiWindow::saveNewToolbarConfig);
270 }
271 d->toolBarEditor->show();
272}
273
274void KXmlGuiWindow::saveNewToolbarConfig()
275{
276 // createGUI(xmlFile()); // this loses any plugged-in guiclients, so we use remove+add instead.
277
278 guiFactory()->removeClient(client: this);
279 guiFactory()->addClient(client: this);
280
281 KConfigGroup cg(KSharedConfig::openConfig(), QString());
282 applyMainWindowSettings(config: cg);
283}
284
285void KXmlGuiWindow::setupGUI(StandardWindowOptions options, const QString &xmlfile)
286{
287 setupGUI(defaultSize: QSize(), options, xmlfile);
288}
289
290void KXmlGuiWindow::setupGUI(const QSize &defaultSize, StandardWindowOptions options, const QString &xmlfile)
291{
292 Q_D(KXmlGuiWindow);
293
294 if (options & Keys) {
295 KStandardAction::keyBindings(recvr: guiFactory(), slot: &KXMLGUIFactory::showConfigureShortcutsDialog, parent: actionCollection());
296 }
297
298 if ((options & StatusBar) && statusBar()) {
299 createStandardStatusBarAction();
300 }
301
302 if (options & ToolBar) {
303 setStandardToolBarMenuEnabled(true);
304 KStandardAction::configureToolbars(recvr: this, slot: &KXmlGuiWindow::configureToolbars, parent: actionCollection());
305 }
306
307 d->defaultSize = defaultSize;
308
309 if (options & Create) {
310 createGUI(xmlfile);
311 }
312
313 if (d->defaultSize.isValid()) {
314 resize(d->defaultSize);
315 } else if (isHidden()) {
316 adjustSize();
317 }
318
319 if (options & Save) {
320 const KConfigGroup cg(autoSaveConfigGroup());
321 if (cg.isValid()) {
322 setAutoSaveSettings(group: cg);
323 } else {
324 setAutoSaveSettings();
325 }
326 }
327}
328void KXmlGuiWindow::createGUI(const QString &xmlfile)
329{
330 Q_D(KXmlGuiWindow);
331 // disabling the updates prevents unnecessary redraws
332 // setUpdatesEnabled( false );
333
334 // just in case we are rebuilding, let's remove our old client
335 guiFactory()->removeClient(client: this);
336
337 // make sure to have an empty GUI
338 QMenuBar *mb = menuBar();
339 if (mb) {
340 mb->clear();
341 }
342
343 qDeleteAll(c: toolBars()); // delete all toolbars
344
345 // don't build a help menu unless the user ask for it
346 if (d->showHelpMenu) {
347 delete d->helpMenu;
348 // we always want a help menu
349 d->helpMenu = new KHelpMenu(this, KAboutData::applicationData(), true);
350
351 KActionCollection *actions = actionCollection();
352 QAction *helpContentsAction = d->helpMenu->action(id: KHelpMenu::menuHelpContents);
353 QAction *whatsThisAction = d->helpMenu->action(id: KHelpMenu::menuWhatsThis);
354 QAction *reportBugAction = d->helpMenu->action(id: KHelpMenu::menuReportBug);
355 QAction *switchLanguageAction = d->helpMenu->action(id: KHelpMenu::menuSwitchLanguage);
356 QAction *aboutAppAction = d->helpMenu->action(id: KHelpMenu::menuAboutApp);
357 QAction *aboutKdeAction = d->helpMenu->action(id: KHelpMenu::menuAboutKDE);
358 QAction *donateAction = d->helpMenu->action(id: KHelpMenu::menuDonate);
359
360 if (helpContentsAction) {
361 actions->addAction(name: helpContentsAction->objectName(), action: helpContentsAction);
362 }
363 if (whatsThisAction) {
364 actions->addAction(name: whatsThisAction->objectName(), action: whatsThisAction);
365 }
366 if (reportBugAction) {
367 actions->addAction(name: reportBugAction->objectName(), action: reportBugAction);
368 }
369 if (switchLanguageAction) {
370 actions->addAction(name: switchLanguageAction->objectName(), action: switchLanguageAction);
371 }
372 if (aboutAppAction) {
373 actions->addAction(name: aboutAppAction->objectName(), action: aboutAppAction);
374 }
375 if (aboutKdeAction) {
376 actions->addAction(name: aboutKdeAction->objectName(), action: aboutKdeAction);
377 }
378 if (donateAction) {
379 actions->addAction(name: donateAction->objectName(), action: donateAction);
380 }
381 }
382
383 const QString windowXmlFile = xmlfile.isNull() ? componentName() + QLatin1String("ui.rc") : xmlfile;
384
385 // Help beginners who call setXMLFile and then setupGUI...
386 if (!xmlFile().isEmpty() && xmlFile() != windowXmlFile) {
387 qCWarning(DEBUG_KXMLGUI) << "You called setXMLFile(" << xmlFile() << ") and then createGUI or setupGUI,"
388 << "which also calls setXMLFile and will overwrite the file you have previously set.\n"
389 << "You should call createGUI(" << xmlFile() << ") or setupGUI(<options>," << xmlFile() << ") instead.";
390 }
391
392 // we always want to load in our global standards file
393 loadStandardsXmlFile();
394
395 // now, merge in our local xml file.
396 setXMLFile(file: windowXmlFile, merge: true);
397
398 // make sure we don't have any state saved already
399 setXMLGUIBuildDocument(QDomDocument());
400
401 // do the actual GUI building
402 guiFactory()->reset();
403 guiFactory()->addClient(client: this);
404
405 checkAmbiguousShortcuts();
406
407 // setUpdatesEnabled( true );
408}
409
410void KXmlGuiWindow::slotStateChanged(const QString &newstate)
411{
412 stateChanged(newstate, reverse: KXMLGUIClient::StateNoReverse);
413}
414
415void KXmlGuiWindow::slotStateChanged(const QString &newstate, bool reverse)
416{
417 stateChanged(newstate, reverse: reverse ? KXMLGUIClient::StateReverse : KXMLGUIClient::StateNoReverse);
418}
419
420void KXmlGuiWindow::setStandardToolBarMenuEnabled(bool showToolBarMenu)
421{
422 Q_D(KXmlGuiWindow);
423 if (showToolBarMenu) {
424 if (d->toolBarHandler) {
425 return;
426 }
427
428 d->toolBarHandler = new KDEPrivate::ToolBarHandler(this);
429
430 if (factory()) {
431 factory()->addClient(client: d->toolBarHandler);
432 }
433 } else {
434 if (!d->toolBarHandler) {
435 return;
436 }
437
438 if (factory()) {
439 factory()->removeClient(client: d->toolBarHandler);
440 }
441
442 delete d->toolBarHandler;
443 d->toolBarHandler = nullptr;
444 }
445}
446
447bool KXmlGuiWindow::isStandardToolBarMenuEnabled() const
448{
449 Q_D(const KXmlGuiWindow);
450 return (d->toolBarHandler);
451}
452
453void KXmlGuiWindow::createStandardStatusBarAction()
454{
455 Q_D(KXmlGuiWindow);
456 if (!d->showStatusBarAction) {
457 d->showStatusBarAction = KStandardAction::showStatusbar(recvr: this, slot: &KMainWindow::setSettingsDirty, parent: actionCollection());
458 QStatusBar *sb = statusBar(); // Creates statusbar if it doesn't exist already.
459 connect(sender: d->showStatusBarAction, signal: &QAction::toggled, context: sb, slot: &QWidget::setVisible);
460 d->showStatusBarAction->setChecked(sb->isHidden());
461 } else {
462 // If the language has changed, we'll need to grab the new text and whatsThis
463 QAction *tmpStatusBar = KStandardAction::showStatusbar(recvr: nullptr, slot: nullptr, parent: nullptr);
464 d->showStatusBarAction->setText(tmpStatusBar->text());
465 d->showStatusBarAction->setWhatsThis(tmpStatusBar->whatsThis());
466 delete tmpStatusBar;
467 }
468}
469
470void KXmlGuiWindow::finalizeGUI(bool /*force*/)
471{
472 // FIXME: this really needs to be removed with a code more like the one we had on KDE3.
473 // what we need to do here is to position correctly toolbars so they don't overlap.
474 // Also, take in count plugins could provide their own toolbars and those also need to
475 // be restored.
476 if (autoSaveSettings() && autoSaveConfigGroup().isValid()) {
477 applyMainWindowSettings(config: autoSaveConfigGroup());
478 }
479}
480
481void KXmlGuiWindow::applyMainWindowSettings(const KConfigGroup &config)
482{
483 Q_D(KXmlGuiWindow);
484 KMainWindow::applyMainWindowSettings(config);
485 QStatusBar *sb = findChild<QStatusBar *>();
486 if (sb && d->showStatusBarAction) {
487 d->showStatusBarAction->setChecked(!sb->isHidden());
488 }
489}
490
491void KXmlGuiWindow::checkAmbiguousShortcuts()
492{
493 QMap<QString, QAction *> shortcuts;
494 QAction *editCutAction = actionCollection()->action(QStringLiteral("edit_cut"));
495 QAction *deleteFileAction = actionCollection()->action(QStringLiteral("deletefile"));
496 const auto actions = actionCollection()->actions();
497 for (QAction *action : actions) {
498 if (action->isEnabled()) {
499 const auto actionShortcuts = action->shortcuts();
500 for (const QKeySequence &shortcut : actionShortcuts) {
501 if (shortcut.isEmpty()) {
502 continue;
503 }
504 const QString portableShortcutText = shortcut.toString();
505 const QAction *existingShortcutAction = shortcuts.value(key: portableShortcutText);
506 if (existingShortcutAction) {
507 // If the shortcut is already in use we give a warning, so that hopefully the developer will find it
508 // There is one exception, if the conflicting shortcut is a non primary shortcut of "edit_cut"
509 // and "deleteFileAction" is the other action since Shift+Delete is used for both in our default code
510 bool showWarning = true;
511 if ((action == editCutAction && existingShortcutAction == deleteFileAction)
512 || (action == deleteFileAction && existingShortcutAction == editCutAction)) {
513 QList<QKeySequence> editCutActionShortcuts = editCutAction->shortcuts();
514 if (editCutActionShortcuts.indexOf(t: shortcut) > 0) // alternate shortcut
515 {
516 editCutActionShortcuts.removeAll(t: shortcut);
517 editCutAction->setShortcuts(editCutActionShortcuts);
518
519 showWarning = false;
520 }
521 }
522
523 if (showWarning) {
524 const QString actionName = KLocalizedString::removeAcceleratorMarker(label: action->text());
525 const QString existingShortcutActionName = KLocalizedString::removeAcceleratorMarker(label: existingShortcutAction->text());
526 QString dontShowAgainString = existingShortcutActionName + actionName + shortcut.toString();
527 dontShowAgainString.remove(c: QLatin1Char('\\'));
528 KMessageBox::information(parent: this,
529 i18n("There are two actions (%1, %2) that want to use the same shortcut (%3). This is most probably a bug. "
530 "Please report it in <a href='https://bugs.kde.org'>bugs.kde.org</a>",
531 existingShortcutActionName,
532 actionName,
533 shortcut.toString(QKeySequence::NativeText)),
534 i18n("Ambiguous Shortcuts"),
535 dontShowAgainName: dontShowAgainString,
536 options: KMessageBox::Notify | KMessageBox::AllowLink);
537 }
538 } else {
539 shortcuts.insert(key: portableShortcutText, value: action);
540 }
541 }
542 }
543 }
544}
545
546void KXmlGuiWindow::setCommandBarEnabled(bool showCommandBar)
547{
548 /**
549 * Unset the shortcut
550 */
551 auto cmdBarAction = actionCollection()->action(QStringLiteral("open_kcommand_bar"));
552 if (showCommandBar) {
553 KActionCollection::setDefaultShortcut(action: cmdBarAction, shortcut: Qt::CTRL | Qt::ALT | Qt::Key_I);
554 } else {
555 KActionCollection::setDefaultShortcut(action: cmdBarAction, shortcut: {});
556 }
557
558 Q_D(KXmlGuiWindow);
559 d->commandBarEnabled = showCommandBar;
560}
561
562bool KXmlGuiWindow::isCommandBarEnabled() const
563{
564 Q_D(const KXmlGuiWindow);
565 return d->commandBarEnabled;
566}
567
568#include "moc_kxmlguiwindow.cpp"
569

source code of kxmlgui/src/kxmlguiwindow.cpp