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 | */ |
56 | static 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 * = action->menu()) { |
76 | const QList<QAction *> = menu->actions(); |
77 | |
78 | ActionGroup ; |
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 | |
107 | static 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 | |
124 | class KXmlGuiWindowPrivate : public KMainWindowPrivate |
125 | { |
126 | public: |
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 : 1; |
140 | QSize defaultSize; |
141 | |
142 | KDEPrivate::ToolBarHandler *toolBarHandler; |
143 | KToggleAction *showStatusBarAction; |
144 | QPointer<KEditToolBar> toolBarEditor; |
145 | KXMLGUIFactory *factory; |
146 | }; |
147 | |
148 | KXmlGuiWindow::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 | |
195 | QAction *KXmlGuiWindow::() |
196 | { |
197 | Q_D(KXmlGuiWindow); |
198 | if (!d->toolBarHandler) { |
199 | return nullptr; |
200 | } |
201 | |
202 | return d->toolBarHandler->toolBarMenuAction(); |
203 | } |
204 | |
205 | void KXmlGuiWindow::() |
206 | { |
207 | Q_D(KXmlGuiWindow); |
208 | if (d->toolBarHandler) { |
209 | d->toolBarHandler->setupActions(); |
210 | } |
211 | } |
212 | |
213 | KXmlGuiWindow::~KXmlGuiWindow() |
214 | { |
215 | Q_D(KXmlGuiWindow); |
216 | delete d->factory; |
217 | } |
218 | |
219 | bool 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 | |
237 | void KXmlGuiWindow::(bool ) |
238 | { |
239 | Q_D(KXmlGuiWindow); |
240 | d->showHelpMenu = showHelpMenu; |
241 | } |
242 | |
243 | bool KXmlGuiWindow::() const |
244 | { |
245 | Q_D(const KXmlGuiWindow); |
246 | return d->showHelpMenu; |
247 | } |
248 | |
249 | KXMLGUIFactory *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 | |
261 | void 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 | |
274 | void 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 | |
285 | void KXmlGuiWindow::setupGUI(StandardWindowOptions options, const QString &xmlfile) |
286 | { |
287 | setupGUI(defaultSize: QSize(), options, xmlfile); |
288 | } |
289 | |
290 | void 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 | } |
328 | void 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 | |
410 | void KXmlGuiWindow::slotStateChanged(const QString &newstate) |
411 | { |
412 | stateChanged(newstate, reverse: KXMLGUIClient::StateNoReverse); |
413 | } |
414 | |
415 | void KXmlGuiWindow::slotStateChanged(const QString &newstate, bool reverse) |
416 | { |
417 | stateChanged(newstate, reverse: reverse ? KXMLGUIClient::StateReverse : KXMLGUIClient::StateNoReverse); |
418 | } |
419 | |
420 | void KXmlGuiWindow::setStandardToolBarMenuEnabled(bool ) |
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 | |
447 | bool KXmlGuiWindow::isStandardToolBarMenuEnabled() const |
448 | { |
449 | Q_D(const KXmlGuiWindow); |
450 | return (d->toolBarHandler); |
451 | } |
452 | |
453 | void 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 | |
470 | void 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 | |
481 | void 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 | |
491 | void 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 | |
546 | void 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 | |
562 | bool KXmlGuiWindow::isCommandBarEnabled() const |
563 | { |
564 | Q_D(const KXmlGuiWindow); |
565 | return d->commandBarEnabled; |
566 | } |
567 | |
568 | #include "moc_kxmlguiwindow.cpp" |
569 | |