1 | /* |
2 | This file is part of the KDE project |
3 | SPDX-FileCopyrightText: 2021 Felix Ernst <fe.a.ernst@gmail.com> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
6 | */ |
7 | |
8 | #include "khamburgermenu.h" |
9 | #include "khamburgermenu_p.h" |
10 | |
11 | #include "khamburgermenuhelpers_p.h" |
12 | |
13 | #include <KLocalizedString> |
14 | |
15 | #include <QMenu> |
16 | #include <QMenuBar> |
17 | #include <QStyle> |
18 | #include <QToolBar> |
19 | #include <QToolButton> |
20 | |
21 | #include <algorithm> |
22 | #include <forward_list> |
23 | #include <unordered_set> |
24 | |
25 | KHamburgerMenu::(QObject *parent) |
26 | : QWidgetAction{parent} |
27 | , d_ptr{new KHamburgerMenuPrivate(this)} |
28 | { |
29 | } |
30 | |
31 | KHamburgerMenuPrivate::(KHamburgerMenu *qq) |
32 | : q_ptr{qq} |
33 | , m_listeners{new ListenerContainer(this)} |
34 | { |
35 | q_ptr->setPriority(QAction::LowPriority); |
36 | connect(sender: q_ptr, signal: &QAction::changed, context: this, slot: &KHamburgerMenuPrivate::slotActionChanged); |
37 | connect(sender: q_ptr, signal: &QAction::triggered, context: this, slot: &KHamburgerMenuPrivate::slotActionTriggered); |
38 | } |
39 | |
40 | KHamburgerMenu::() = default; |
41 | |
42 | KHamburgerMenuPrivate::() = default; |
43 | |
44 | void KHamburgerMenu::(QMenuBar *) |
45 | { |
46 | Q_D(KHamburgerMenu); |
47 | d->setMenuBar(menuBar); |
48 | } |
49 | |
50 | void KHamburgerMenuPrivate::(QMenuBar *) |
51 | { |
52 | if (m_menuBar) { |
53 | m_menuBar->removeEventFilter(obj: m_listeners->get<VisibilityChangesListener>()); |
54 | m_menuBar->removeEventFilter(obj: m_listeners->get<AddOrRemoveActionListener>()); |
55 | } |
56 | m_menuBar = menuBar; |
57 | updateVisibility(); |
58 | if (m_menuBar) { |
59 | m_menuBar->installEventFilter(filterObj: m_listeners->get<VisibilityChangesListener>()); |
60 | m_menuBar->installEventFilter(filterObj: m_listeners->get<AddOrRemoveActionListener>()); |
61 | } |
62 | } |
63 | |
64 | QMenuBar *KHamburgerMenu::() const |
65 | { |
66 | Q_D(const KHamburgerMenu); |
67 | return d->menuBar(); |
68 | } |
69 | |
70 | QMenuBar *KHamburgerMenuPrivate::() const |
71 | { |
72 | return m_menuBar; |
73 | } |
74 | |
75 | void KHamburgerMenu::(bool advertise) |
76 | { |
77 | Q_D(KHamburgerMenu); |
78 | d->setMenuBarAdvertised(advertise); |
79 | } |
80 | |
81 | void KHamburgerMenuPrivate::(bool advertise) |
82 | { |
83 | m_advertiseMenuBar = advertise; |
84 | } |
85 | |
86 | bool KHamburgerMenu::() const |
87 | { |
88 | Q_D(const KHamburgerMenu); |
89 | return d->menuBarAdvertised(); |
90 | } |
91 | |
92 | bool KHamburgerMenuPrivate::() const |
93 | { |
94 | return m_advertiseMenuBar; |
95 | } |
96 | |
97 | void KHamburgerMenu::(QAction *) |
98 | { |
99 | Q_D(KHamburgerMenu); |
100 | d->setShowMenuBarAction(showMenuBarAction); |
101 | } |
102 | |
103 | void KHamburgerMenuPrivate::(QAction *) |
104 | { |
105 | m_showMenuBarAction = showMenuBarAction; |
106 | } |
107 | |
108 | void KHamburgerMenu::(QMenu *) |
109 | { |
110 | Q_D(KHamburgerMenu); |
111 | d->insertIntoMenuBefore(menu, before: nullptr); |
112 | } |
113 | |
114 | void KHamburgerMenu::(QMenu *, QAction *before) |
115 | { |
116 | Q_D(KHamburgerMenu); |
117 | d->insertIntoMenuBefore(menu, before); |
118 | } |
119 | |
120 | void KHamburgerMenuPrivate::(QMenu *, QAction *before) |
121 | { |
122 | Q_CHECK_PTR(menu); |
123 | Q_Q(KHamburgerMenu); |
124 | if (!m_menuAction) { |
125 | m_menuAction = new QAction(this); |
126 | m_menuAction->setText(i18nc("@action:inmenu General purpose menu" , "&Menu" )); |
127 | m_menuAction->setIcon(q->icon()); |
128 | m_menuAction->setMenu(m_actualMenu.get()); |
129 | } |
130 | updateVisibility(); // Sets the appropriate visibility of m_menuAction. |
131 | |
132 | menu->insertAction(before, action: m_menuAction); |
133 | connect(sender: menu, signal: &QMenu::aboutToShow, context: this, slot: [this, menu, q]() { |
134 | if (m_menuAction->isVisible()) { |
135 | Q_EMIT q->aboutToShowMenu(); |
136 | hideActionsOf(widget: menu); |
137 | resetMenu(); |
138 | } |
139 | }); |
140 | } |
141 | |
142 | void KHamburgerMenu::(QWidget *widget) |
143 | { |
144 | Q_D(KHamburgerMenu); |
145 | d->hideActionsOf(widget); |
146 | } |
147 | |
148 | void KHamburgerMenuPrivate::(QWidget *widget) |
149 | { |
150 | Q_CHECK_PTR(widget); |
151 | m_widgetsWithActionsToBeHidden.remove(val: nullptr); |
152 | if (listContainsWidget(list: m_widgetsWithActionsToBeHidden, widget)) { |
153 | return; |
154 | } |
155 | m_widgetsWithActionsToBeHidden.emplace_front(args: QPointer<const QWidget>(widget)); |
156 | if (QMenu * = qobject_cast<QMenu *>(object: widget)) { |
157 | // QMenus are normally hidden. This will avoid redundancy with their actions anyways. |
158 | menu->installEventFilter(filterObj: m_listeners->get<AddOrRemoveActionListener>()); |
159 | notifyMenuResetNeeded(); |
160 | } else { |
161 | // Only avoid redundancy when the widget is visible. |
162 | widget->installEventFilter(filterObj: m_listeners->get<VisibleActionsChangeListener>()); |
163 | if (widget->isVisible()) { |
164 | notifyMenuResetNeeded(); |
165 | } |
166 | } |
167 | } |
168 | |
169 | void KHamburgerMenu::(QWidget *widget) |
170 | { |
171 | Q_D(KHamburgerMenu); |
172 | d->showActionsOf(widget); |
173 | } |
174 | |
175 | void KHamburgerMenuPrivate::(QWidget *widget) |
176 | { |
177 | Q_CHECK_PTR(widget); |
178 | m_widgetsWithActionsToBeHidden.remove(val: widget); |
179 | widget->removeEventFilter(obj: m_listeners->get<AddOrRemoveActionListener>()); |
180 | widget->removeEventFilter(obj: m_listeners->get<VisibleActionsChangeListener>()); |
181 | if (isWidgetActuallyVisible(widget)) { |
182 | notifyMenuResetNeeded(); |
183 | } |
184 | } |
185 | |
186 | QWidget *KHamburgerMenu::(QWidget *parent) |
187 | { |
188 | Q_D(KHamburgerMenu); |
189 | return d->createWidget(parent); |
190 | } |
191 | |
192 | QWidget *KHamburgerMenuPrivate::(QWidget *parent) |
193 | { |
194 | if (qobject_cast<QMenu *>(object: parent)) { |
195 | qDebug( |
196 | msg: "Adding a KHamburgerMenu directly to a QMenu. " |
197 | "This will look odd. Use addToMenu() instead." ); |
198 | } |
199 | Q_Q(KHamburgerMenu); |
200 | |
201 | auto toolButton = new QToolButton(parent); |
202 | // Set appearance |
203 | toolButton->setDefaultAction(q); |
204 | toolButton->setMenu(m_actualMenu.get()); |
205 | toolButton->setAttribute(Qt::WidgetAttribute::WA_CustomWhatsThis); |
206 | toolButton->setPopupMode(QToolButton::InstantPopup); |
207 | updateButtonStyle(hamburgerMenuButton: toolButton); |
208 | if (const QToolBar *toolbar = qobject_cast<QToolBar *>(object: parent)) { |
209 | connect(sender: toolbar, signal: &QToolBar::toolButtonStyleChanged, context: toolButton, slot: &QToolButton::setToolButtonStyle); |
210 | } |
211 | |
212 | setToolButtonVisible(toolButton, visible: !isMenuBarVisible(menuBar: m_menuBar)); |
213 | |
214 | // Make sure the menu will be ready in time |
215 | toolButton->installEventFilter(filterObj: m_listeners->get<ButtonPressListener>()); |
216 | |
217 | hideActionsOf(widget: parent); |
218 | return toolButton; |
219 | } |
220 | |
221 | QAction *KHamburgerMenuPrivate::(QAction *from, QWidget *parent, std::unordered_set<const QAction *> &nonExclusives) const |
222 | { |
223 | Q_CHECK_PTR(from); |
224 | if (nonExclusives.count(x: from) > 0) { |
225 | return nullptr; // The action is non-exclusive/already visible elsewhere. |
226 | } |
227 | if (!from->menu() || from->menu()->isEmpty()) { |
228 | return from; // The action is exclusive and doesn't have a menu. |
229 | } |
230 | std::unique_ptr<QAction> (new QAction(from->icon(), from->text(), parent)); |
231 | std::unique_ptr<QMenu> (new QMenu(parent)); |
232 | const auto = from->menu()->actions(); |
233 | for (QAction *action : fromMenuActions) { |
234 | QAction *actionWithExclusives = actionWithExclusivesFrom(from: action, parent: menuWithExclusives.get(), nonExclusives); |
235 | if (actionWithExclusives) { |
236 | menuWithExclusives->addAction(action: actionWithExclusives); |
237 | } |
238 | } |
239 | if (menuWithExclusives->isEmpty()) { |
240 | return nullptr; // "from" has a menu that contains zero exclusive actions. |
241 | // There is a chance that "from" is an exclusive action itself and should |
242 | // therefore be returned instead but that is unlikely for an action that has a menu(). |
243 | // This fringe case is the only one that can't be correctly covered because we can |
244 | // not know or assume that activating the action does something or if it is nothing |
245 | // but a container for a menu. |
246 | } |
247 | menuActionWithExclusives->setMenu(menuWithExclusives.release()); |
248 | return menuActionWithExclusives.release(); |
249 | } |
250 | |
251 | std::unique_ptr<QMenu> KHamburgerMenuPrivate::() |
252 | { |
253 | std::unique_ptr<QMenu> (new QMenu()); |
254 | Q_Q(const KHamburgerMenu); |
255 | |
256 | // Make sure we notice if the q->menu() is changed or replaced in the future. |
257 | if (q->menu() != m_lastUsedMenu) { |
258 | q->menu()->installEventFilter(filterObj: m_listeners->get<AddOrRemoveActionListener>()); |
259 | |
260 | if (m_lastUsedMenu && !listContainsWidget(list: m_widgetsWithActionsToBeHidden, widget: m_lastUsedMenu)) { |
261 | m_lastUsedMenu->removeEventFilter(obj: m_listeners->get<AddOrRemoveActionListener>()); |
262 | } |
263 | m_lastUsedMenu = q->menu(); |
264 | } |
265 | |
266 | if (!q->menu() && !m_menuBar) { |
267 | return menu; // empty menu |
268 | } |
269 | |
270 | if (!q->menu()) { |
271 | // We have nothing else to work with so let's just add the menuBar contents. |
272 | const auto = m_menuBar->actions(); |
273 | for (QAction * : menuBarActions) { |
274 | menu->addAction(action: menuAction); |
275 | } |
276 | return menu; |
277 | } |
278 | |
279 | // Collect actions which shouldn't be added to the menu |
280 | std::unordered_set<const QAction *> visibleActions; |
281 | m_widgetsWithActionsToBeHidden.remove(val: nullptr); |
282 | for (const QWidget *widget : m_widgetsWithActionsToBeHidden) { |
283 | if (qobject_cast<const QMenu *>(object: widget) || isWidgetActuallyVisible(widget)) { |
284 | // avoid redundancy with menus even when they are not actually visible. |
285 | visibleActions.reserve(n: visibleActions.size() + widget->actions().size()); |
286 | const auto widgetActions = widget->actions(); |
287 | for (QAction *action : widgetActions) { |
288 | visibleActions.insert(x: action); |
289 | } |
290 | } |
291 | } |
292 | // Populate the menu |
293 | const auto = q->menu()->actions(); |
294 | for (QAction *action : menuActions) { |
295 | if (visibleActions.count(x: action) == 0) { |
296 | menu->addAction(action); |
297 | visibleActions.insert(x: action); |
298 | } |
299 | } |
300 | // Add the last two menu actions |
301 | if (m_menuBar) { |
302 | connect(sender: menu.get(), signal: &QMenu::aboutToShow, context: this, slot: [this]() { |
303 | if (m_menuBar->actions().last()->icon().isNull()) { |
304 | m_helpIconIsSet = false; |
305 | m_menuBar->actions().last()->setIcon(QIcon::fromTheme(QStringLiteral("help-contents" ))); // set "Help" menu icon |
306 | } else { |
307 | m_helpIconIsSet = true; // if the "Help" icon was set by the application, we want to leave it untouched |
308 | } |
309 | }); |
310 | connect(sender: menu.get(), signal: &QMenu::aboutToHide, context: this, slot: [this]() { |
311 | if (m_menuBar->actions().last()->icon().name() == QStringLiteral("help-contents" ) && !m_helpIconIsSet) { |
312 | m_menuBar->actions().last()->setIcon(QIcon()); |
313 | } |
314 | }); |
315 | menu->addAction(action: m_menuBar->actions().last()); // add "Help" menu |
316 | visibleActions.insert(x: m_menuBar->actions().last()); |
317 | if (m_advertiseMenuBar) { |
318 | menu->addSeparator(); |
319 | m_menuBarAdvertisementMenu = newMenuBarAdvertisementMenu(visibleActions); |
320 | menu->addAction(action: m_menuBarAdvertisementMenu->menuAction()); |
321 | } |
322 | } |
323 | return menu; |
324 | } |
325 | |
326 | std::unique_ptr<QMenu> KHamburgerMenuPrivate::(std::unordered_set<const QAction *> &visibleActions) |
327 | { |
328 | std::unique_ptr<QMenu> (new QMenu()); |
329 | m_showMenuBarWithAllActionsText = i18nc("@action:inmenu A menu item that advertises and enables the menubar" , "Show &Menubar with All Actions" ); |
330 | connect(sender: advertiseMenuBarMenu.get(), signal: &QMenu::aboutToShow, context: this, slot: [this]() { |
331 | if (m_showMenuBarAction) { |
332 | m_showMenuBarText = m_showMenuBarAction->text(); |
333 | m_showMenuBarAction->setText(m_showMenuBarWithAllActionsText); |
334 | } |
335 | }); |
336 | connect(sender: advertiseMenuBarMenu.get(), signal: &QMenu::aboutToHide, context: this, slot: [this]() { |
337 | if (m_showMenuBarAction && m_showMenuBarAction->text() == m_showMenuBarWithAllActionsText) { |
338 | m_showMenuBarAction->setText(m_showMenuBarText); |
339 | } |
340 | }); |
341 | if (m_showMenuBarAction) { |
342 | advertiseMenuBarMenu->addAction(action: m_showMenuBarAction); |
343 | visibleActions.insert(x: m_showMenuBarAction); |
344 | } |
345 | QAction *section = advertiseMenuBarMenu->addSeparator(); |
346 | |
347 | const auto = m_menuBar->actions(); |
348 | for (QAction * : menuBarActions) { |
349 | QAction * = actionWithExclusivesFrom(from: menuAction, parent: advertiseMenuBarMenu.get(), nonExclusives&: visibleActions); |
350 | if (menuActionWithExclusives) { |
351 | advertiseMenuBarMenu->addAction(action: menuActionWithExclusives); |
352 | } |
353 | } |
354 | advertiseMenuBarMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic" ))); |
355 | advertiseMenuBarMenu->setTitle(i18nc("@action:inmenu A menu text advertising its contents (more features)." , "More" )); |
356 | section->setText(i18nc("@action:inmenu A section heading advertising the contents of the menu bar" , "More Actions" )); |
357 | return advertiseMenuBarMenu; |
358 | } |
359 | |
360 | void KHamburgerMenuPrivate::() |
361 | { |
362 | Q_Q(KHamburgerMenu); |
363 | if (!m_menuResetNeeded && m_actualMenu && m_lastUsedMenu == q->menu()) { |
364 | return; |
365 | } |
366 | m_menuResetNeeded = false; |
367 | |
368 | m_actualMenu = newMenu(); |
369 | |
370 | const auto createdWidgets = q->createdWidgets(); |
371 | for (auto widget : createdWidgets) { |
372 | static_cast<QToolButton *>(widget)->setMenu(m_actualMenu.get()); |
373 | } |
374 | if (m_menuAction) { |
375 | m_menuAction->setMenu(m_actualMenu.get()); |
376 | } |
377 | } |
378 | |
379 | void KHamburgerMenuPrivate::() |
380 | { |
381 | Q_Q(KHamburgerMenu); |
382 | /** The visibility of KHamburgerMenu should be opposite to the visibility of m_menuBar. |
383 | * Exception: We only consider a visible m_menuBar as actually visible if it is not a native |
384 | * menu bar because native menu bars can come in many shapes and sizes which don't necessarily |
385 | * have the same usability benefits as a traditional in-window menu bar. |
386 | * KDE applications normally allow the user to remove any actions from their toolbar(s) anyway. */ |
387 | const bool = isMenuBarVisible(menuBar: m_menuBar); |
388 | |
389 | const auto createdWidgets = q->createdWidgets(); |
390 | for (auto widget : createdWidgets) { |
391 | setToolButtonVisible(toolButton: widget, visible: !menuBarVisible); |
392 | } |
393 | |
394 | if (!m_menuAction) { |
395 | if (menuBarVisible && m_actualMenu) { |
396 | m_actualMenu.release()->deleteLater(); // might as well free up some memory |
397 | } |
398 | return; |
399 | } |
400 | |
401 | // The m_menuAction acts as a fallback if both the m_menuBar and all createdWidgets() on the UI |
402 | // are currently hidden. Only then should the m_menuAction ever be visible in a QMenu. |
403 | if (menuBarVisible || (m_menuBar && m_menuBar->isNativeMenuBar()) // See [1] below. |
404 | || std::any_of(first: createdWidgets.cbegin(), last: createdWidgets.cend(), pred: isWidgetActuallyVisible)) { |
405 | m_menuAction->setVisible(false); |
406 | return; |
407 | } |
408 | m_menuAction->setVisible(true); |
409 | |
410 | // [1] While the m_menuAction can be used as a normal menu by users that don't mind invoking a |
411 | // QMenu to access any menu actions, its primary use really is that of a fallback. |
412 | // Therefore the existence of a native menu bar (no matter what shape or size it might have) |
413 | // is enough reason for us to hide m_menuAction. |
414 | } |
415 | |
416 | void KHamburgerMenuPrivate::() |
417 | { |
418 | Q_Q(KHamburgerMenu); |
419 | const auto createdWidgets = q->createdWidgets(); |
420 | for (auto widget : createdWidgets) { |
421 | auto toolButton = static_cast<QToolButton *>(widget); |
422 | updateButtonStyle(hamburgerMenuButton: toolButton); |
423 | } |
424 | } |
425 | |
426 | void KHamburgerMenuPrivate::() |
427 | { |
428 | if (isMenuBarVisible(menuBar: m_menuBar)) { |
429 | const auto = m_menuBar->actions(); |
430 | for (const auto action : menuBarActions) { |
431 | if (action->isEnabled() && !action->isSeparator()) { |
432 | m_menuBar->setActiveAction(m_menuBar->actions().constFirst()); |
433 | return; |
434 | } |
435 | } |
436 | } |
437 | |
438 | Q_Q(KHamburgerMenu); |
439 | const auto createdWidgets = q->createdWidgets(); |
440 | for (auto widget : createdWidgets) { |
441 | if (isWidgetActuallyVisible(widget) && widget->isActiveWindow()) { |
442 | auto toolButton = static_cast<QToolButton *>(widget); |
443 | m_listeners->get<ButtonPressListener>()->prepareHamburgerButtonForPress(button: toolButton); |
444 | toolButton->pressed(); |
445 | return; |
446 | } |
447 | } |
448 | |
449 | Q_EMIT q->aboutToShowMenu(); |
450 | resetMenu(); |
451 | prepareParentlessMenuForShowing(menu: m_actualMenu.get(), surrogateParent: nullptr); |
452 | m_actualMenu->popup(pos: QCursor::pos()); |
453 | } |
454 | |
455 | void KHamburgerMenuPrivate::(QToolButton *) const |
456 | { |
457 | Q_Q(const KHamburgerMenu); |
458 | Qt::ToolButtonStyle buttonStyle = Qt::ToolButtonFollowStyle; |
459 | if (QToolBar *toolbar = qobject_cast<QToolBar *>(object: hamburgerMenuButton->parent())) { |
460 | buttonStyle = toolbar->toolButtonStyle(); |
461 | } |
462 | if (buttonStyle == Qt::ToolButtonFollowStyle) { |
463 | buttonStyle = static_cast<Qt::ToolButtonStyle>(hamburgerMenuButton->style()->styleHint(stylehint: QStyle::SH_ToolButtonStyle)); |
464 | } |
465 | if (buttonStyle == Qt::ToolButtonTextBesideIcon && q->priority() < QAction::NormalPriority) { |
466 | hamburgerMenuButton->setToolButtonStyle(Qt::ToolButtonIconOnly); |
467 | } else { |
468 | hamburgerMenuButton->setToolButtonStyle(buttonStyle); |
469 | } |
470 | } |
471 | |
472 | #include "moc_khamburgermenu.cpp" |
473 | #include "moc_khamburgermenu_p.cpp" |
474 | |