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 <KStandardActions>
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
213bool KXmlGuiWindow::isToolBarVisible(const QString &name)
214{
215 KToolBar *tb = findChild<KToolBar *>(aName: name);
216 if (!tb) {
217 return false;
218 }
219
220 return tb->isVisible();
221}
222
223void KXmlGuiWindow::setToolBarVisible(const QString &name, bool visible)
224{
225 KToolBar *tb = findChild<KToolBar *>(aName: name);
226 if (!tb) {
227 return;
228 }
229
230 tb->setVisible(visible);
231}
232
233QStringList KXmlGuiWindow::toolBarNames() const
234{
235 QList<KToolBar *> bars = toolBars();
236 QStringList ret;
237
238 for (KToolBar *bar : bars) {
239 ret.append(t: bar->objectName());
240 }
241
242 return ret;
243}
244
245KXmlGuiWindow::~KXmlGuiWindow()
246{
247 Q_D(KXmlGuiWindow);
248 delete d->factory;
249}
250
251bool KXmlGuiWindow::event(QEvent *ev)
252{
253 bool ret = KMainWindow::event(event: ev);
254 if (ev->type() == QEvent::Polish) {
255#ifdef WITH_QTDBUS
256 /* clang-format off */
257 constexpr auto opts = QDBusConnection::ExportScriptableSlots
258 | QDBusConnection::ExportScriptableProperties
259 | QDBusConnection::ExportNonScriptableSlots
260 | QDBusConnection::ExportNonScriptableProperties
261 | QDBusConnection::ExportChildObjects;
262 /* clang-format on */
263 QDBusConnection::sessionBus().registerObject(path: dbusName() + QLatin1String("/actions"), object: actionCollection(), options: opts);
264#endif
265 }
266 return ret;
267}
268
269void KXmlGuiWindow::setHelpMenuEnabled(bool showHelpMenu)
270{
271 Q_D(KXmlGuiWindow);
272 d->showHelpMenu = showHelpMenu;
273}
274
275bool KXmlGuiWindow::isHelpMenuEnabled() const
276{
277 Q_D(const KXmlGuiWindow);
278 return d->showHelpMenu;
279}
280
281KXMLGUIFactory *KXmlGuiWindow::guiFactory()
282{
283 Q_D(KXmlGuiWindow);
284 if (!d->factory) {
285 d->factory = new KXMLGUIFactory(this, this);
286 connect(sender: d->factory, signal: &KXMLGUIFactory::makingChanges, context: this, slot: [d](bool state) {
287 d->slotFactoryMakingChanges(b: state);
288 });
289 }
290 return d->factory;
291}
292
293void KXmlGuiWindow::configureToolbars()
294{
295 Q_D(KXmlGuiWindow);
296 KConfigGroup cg(KSharedConfig::openConfig(), QString());
297 saveMainWindowSettings(config&: cg);
298 if (!d->toolBarEditor) {
299 d->toolBarEditor = new KEditToolBar(guiFactory(), this);
300 d->toolBarEditor->setAttribute(Qt::WA_DeleteOnClose);
301 connect(sender: d->toolBarEditor, signal: &KEditToolBar::newToolBarConfig, context: this, slot: &KXmlGuiWindow::saveNewToolbarConfig);
302 }
303 d->toolBarEditor->show();
304}
305
306void KXmlGuiWindow::saveNewToolbarConfig()
307{
308 // createGUI(xmlFile()); // this loses any plugged-in guiclients, so we use remove+add instead.
309
310 guiFactory()->removeClient(client: this);
311 guiFactory()->addClient(client: this);
312
313 KConfigGroup cg(KSharedConfig::openConfig(), QString());
314 applyMainWindowSettings(config: cg);
315}
316
317void KXmlGuiWindow::setupGUI(StandardWindowOptions options, const QString &xmlfile)
318{
319 setupGUI(defaultSize: QSize(), options, xmlfile);
320}
321
322void KXmlGuiWindow::setupGUI(const QSize &defaultSize, StandardWindowOptions options, const QString &xmlfile)
323{
324 Q_D(KXmlGuiWindow);
325
326 if (options & Keys) {
327 KStandardActions::keyBindings(recvr: guiFactory(), slot: &KXMLGUIFactory::showConfigureShortcutsDialog, parent: actionCollection());
328 }
329
330 if ((options & StatusBar) && statusBar()) {
331 createStandardStatusBarAction();
332 }
333
334 if (options & ToolBar) {
335 setStandardToolBarMenuEnabled(true);
336 KStandardActions::configureToolbars(recvr: this, slot: &KXmlGuiWindow::configureToolbars, parent: actionCollection());
337 }
338
339 d->defaultSize = defaultSize;
340
341 if (options & Create) {
342 createGUI(xmlfile);
343 }
344
345 if (d->defaultSize.isValid()) {
346 resize(d->defaultSize);
347 } else if (isHidden()) {
348 adjustSize();
349 }
350
351 if (options & Save) {
352 const KConfigGroup cg(autoSaveConfigGroup());
353 if (cg.isValid()) {
354 setAutoSaveSettings(group: cg);
355 } else {
356 setAutoSaveSettings();
357 }
358 }
359}
360void KXmlGuiWindow::createGUI(const QString &xmlfile)
361{
362 Q_D(KXmlGuiWindow);
363 // disabling the updates prevents unnecessary redraws
364 // setUpdatesEnabled( false );
365
366 // just in case we are rebuilding, let's remove our old client
367 guiFactory()->removeClient(client: this);
368
369 // make sure to have an empty GUI
370 QMenuBar *mb = menuBar();
371 if (mb) {
372 mb->clear();
373 }
374
375 qDeleteAll(c: toolBars()); // delete all toolbars
376
377 // don't build a help menu unless the user ask for it
378 if (d->showHelpMenu) {
379 delete d->helpMenu;
380 // we always want a help menu
381 d->helpMenu = new KHelpMenu(this);
382
383 KActionCollection *actions = actionCollection();
384 QAction *helpContentsAction = d->helpMenu->action(id: KHelpMenu::menuHelpContents);
385 QAction *whatsThisAction = d->helpMenu->action(id: KHelpMenu::menuWhatsThis);
386 QAction *reportBugAction = d->helpMenu->action(id: KHelpMenu::menuReportBug);
387 QAction *switchLanguageAction = d->helpMenu->action(id: KHelpMenu::menuSwitchLanguage);
388 QAction *aboutAppAction = d->helpMenu->action(id: KHelpMenu::menuAboutApp);
389 QAction *aboutKdeAction = d->helpMenu->action(id: KHelpMenu::menuAboutKDE);
390 QAction *donateAction = d->helpMenu->action(id: KHelpMenu::menuDonate);
391
392 if (helpContentsAction) {
393 actions->addAction(name: helpContentsAction->objectName(), action: helpContentsAction);
394 }
395 if (whatsThisAction) {
396 actions->addAction(name: whatsThisAction->objectName(), action: whatsThisAction);
397 }
398 if (reportBugAction) {
399 actions->addAction(name: reportBugAction->objectName(), action: reportBugAction);
400 }
401 if (switchLanguageAction) {
402 actions->addAction(name: switchLanguageAction->objectName(), action: switchLanguageAction);
403 }
404 if (aboutAppAction) {
405 actions->addAction(name: aboutAppAction->objectName(), action: aboutAppAction);
406 }
407 if (aboutKdeAction) {
408 actions->addAction(name: aboutKdeAction->objectName(), action: aboutKdeAction);
409 }
410 if (donateAction) {
411 actions->addAction(name: donateAction->objectName(), action: donateAction);
412 }
413 }
414
415 const QString windowXmlFile = xmlfile.isNull() ? componentName() + QLatin1String("ui.rc") : xmlfile;
416
417 // Help beginners who call setXMLFile and then setupGUI...
418 if (!xmlFile().isEmpty() && xmlFile() != windowXmlFile) {
419 qCWarning(DEBUG_KXMLGUI) << "You called setXMLFile(" << xmlFile() << ") and then createGUI or setupGUI,"
420 << "which also calls setXMLFile and will overwrite the file you have previously set.\n"
421 << "You should call createGUI(" << xmlFile() << ") or setupGUI(<options>," << xmlFile() << ") instead.";
422 }
423
424 // we always want to load in our global standards file
425 loadStandardsXmlFile();
426
427 // now, merge in our local xml file.
428 setXMLFile(file: windowXmlFile, merge: true);
429
430 // make sure we don't have any state saved already
431 setXMLGUIBuildDocument(QDomDocument());
432
433 // do the actual GUI building
434 guiFactory()->reset();
435 guiFactory()->addClient(client: this);
436
437 checkAmbiguousShortcuts();
438
439 // setUpdatesEnabled( true );
440}
441
442void KXmlGuiWindow::slotStateChanged(const QString &newstate)
443{
444 stateChanged(newstate, reverse: KXMLGUIClient::StateNoReverse);
445}
446
447void KXmlGuiWindow::slotStateChanged(const QString &newstate, bool reverse)
448{
449 stateChanged(newstate, reverse: reverse ? KXMLGUIClient::StateReverse : KXMLGUIClient::StateNoReverse);
450}
451
452void KXmlGuiWindow::setStandardToolBarMenuEnabled(bool showToolBarMenu)
453{
454 Q_D(KXmlGuiWindow);
455 if (showToolBarMenu) {
456 if (d->toolBarHandler) {
457 return;
458 }
459
460 d->toolBarHandler = new KDEPrivate::ToolBarHandler(this);
461
462 if (factory()) {
463 factory()->addClient(client: d->toolBarHandler);
464 }
465 } else {
466 if (!d->toolBarHandler) {
467 return;
468 }
469
470 if (factory()) {
471 factory()->removeClient(client: d->toolBarHandler);
472 }
473
474 delete d->toolBarHandler;
475 d->toolBarHandler = nullptr;
476 }
477}
478
479bool KXmlGuiWindow::isStandardToolBarMenuEnabled() const
480{
481 Q_D(const KXmlGuiWindow);
482 return (d->toolBarHandler);
483}
484
485void KXmlGuiWindow::createStandardStatusBarAction()
486{
487 Q_D(KXmlGuiWindow);
488 if (!d->showStatusBarAction) {
489 d->showStatusBarAction = KStandardAction::showStatusbar(recvr: this, slot: &KMainWindow::setSettingsDirty, parent: actionCollection());
490 QStatusBar *sb = statusBar(); // Creates statusbar if it doesn't exist already.
491 connect(sender: d->showStatusBarAction, signal: &QAction::toggled, context: sb, slot: &QWidget::setVisible);
492 d->showStatusBarAction->setChecked(sb->isHidden());
493 } else {
494 // If the language has changed, we'll need to grab the new text and whatsThis
495 QAction *tmpStatusBar = KStandardAction::showStatusbar(recvr: nullptr, slot: nullptr, parent: nullptr);
496 d->showStatusBarAction->setText(tmpStatusBar->text());
497 d->showStatusBarAction->setWhatsThis(tmpStatusBar->whatsThis());
498 delete tmpStatusBar;
499 }
500}
501
502void KXmlGuiWindow::finalizeGUI(bool /*force*/)
503{
504 // FIXME: this really needs to be removed with a code more like the one we had on KDE3.
505 // what we need to do here is to position correctly toolbars so they don't overlap.
506 // Also, take in count plugins could provide their own toolbars and those also need to
507 // be restored.
508 if (autoSaveSettings() && autoSaveConfigGroup().isValid()) {
509 applyMainWindowSettings(config: autoSaveConfigGroup());
510 }
511}
512
513void KXmlGuiWindow::applyMainWindowSettings(const KConfigGroup &config)
514{
515 Q_D(KXmlGuiWindow);
516 KMainWindow::applyMainWindowSettings(config);
517 QStatusBar *sb = findChild<QStatusBar *>();
518 if (sb && d->showStatusBarAction) {
519 d->showStatusBarAction->setChecked(!sb->isHidden());
520 }
521}
522
523void KXmlGuiWindow::checkAmbiguousShortcuts()
524{
525 QMap<QString, QAction *> shortcuts;
526 QAction *editCutAction = actionCollection()->action(QStringLiteral("edit_cut"));
527 QAction *deleteFileAction = actionCollection()->action(QStringLiteral("deletefile"));
528 const auto actions = actionCollection()->actions();
529 for (QAction *action : actions) {
530 if (action->isEnabled()) {
531 const auto actionShortcuts = action->shortcuts();
532 for (const QKeySequence &shortcut : actionShortcuts) {
533 if (shortcut.isEmpty()) {
534 continue;
535 }
536 const QString portableShortcutText = shortcut.toString();
537 const QAction *existingShortcutAction = shortcuts.value(key: portableShortcutText);
538 if (existingShortcutAction) {
539 // If the shortcut is already in use we give a warning, so that hopefully the developer will find it
540 // There is one exception, if the conflicting shortcut is a non primary shortcut of "edit_cut"
541 // and "deleteFileAction" is the other action since Shift+Delete is used for both in our default code
542 bool showWarning = true;
543 if ((action == editCutAction && existingShortcutAction == deleteFileAction)
544 || (action == deleteFileAction && existingShortcutAction == editCutAction)) {
545 QList<QKeySequence> editCutActionShortcuts = editCutAction->shortcuts();
546 if (editCutActionShortcuts.indexOf(t: shortcut) > 0) // alternate shortcut
547 {
548 editCutActionShortcuts.removeAll(t: shortcut);
549 editCutAction->setShortcuts(editCutActionShortcuts);
550
551 showWarning = false;
552 }
553 }
554
555 if (showWarning) {
556 const QString actionName = KLocalizedString::removeAcceleratorMarker(label: action->text());
557 const QString existingShortcutActionName = KLocalizedString::removeAcceleratorMarker(label: existingShortcutAction->text());
558 QString dontShowAgainString = existingShortcutActionName + actionName + shortcut.toString();
559 dontShowAgainString.remove(c: QLatin1Char('\\'));
560 KMessageBox::information(parent: this,
561 i18n("There are two actions (%1, %2) that want to use the same shortcut (%3). This is most probably a bug. "
562 "Please report it in <a href='https://bugs.kde.org'>bugs.kde.org</a>",
563 existingShortcutActionName,
564 actionName,
565 shortcut.toString(QKeySequence::NativeText)),
566 i18n("Ambiguous Shortcuts"),
567 dontShowAgainName: dontShowAgainString,
568 options: KMessageBox::Notify | KMessageBox::AllowLink);
569 }
570 } else {
571 shortcuts.insert(key: portableShortcutText, value: action);
572 }
573 }
574 }
575 }
576}
577
578void KXmlGuiWindow::setCommandBarEnabled(bool showCommandBar)
579{
580 /*
581 * Unset the shortcut
582 */
583 auto cmdBarAction = actionCollection()->action(QStringLiteral("open_kcommand_bar"));
584 if (showCommandBar) {
585 KActionCollection::setDefaultShortcut(action: cmdBarAction, shortcut: Qt::CTRL | Qt::ALT | Qt::Key_I);
586 } else {
587 KActionCollection::setDefaultShortcut(action: cmdBarAction, shortcut: {});
588 }
589
590 Q_D(KXmlGuiWindow);
591 d->commandBarEnabled = showCommandBar;
592}
593
594bool KXmlGuiWindow::isCommandBarEnabled() const
595{
596 Q_D(const KXmlGuiWindow);
597 return d->commandBarEnabled;
598}
599
600#include "moc_kxmlguiwindow.cpp"
601

source code of kxmlgui/src/kxmlguiwindow.cpp