Line data Source code
1 : // SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Implements MenuBarPeek; an application-wide event filter installed
6 : * in the constructor feeds onKeyPress() and onKeyRelease().
7 : */
8 : #include "menubarpeek.h"
9 : #include <QAction>
10 : #include <QApplication>
11 : #include <QKeyEvent>
12 : #include <QMainWindow>
13 : #include <QMenu>
14 : #include <QMenuBar>
15 : #include <QTimer>
16 :
17 131 : MenuBarPeek::MenuBarPeek(QMainWindow *window, QAction *toggle)
18 131 : : QObject(window), m_window(window), m_toggle(toggle) {
19 : // Key events go to the focus widget, and an open menu is a window of its
20 : // own, so the filter has to sit on the application.
21 131 : qApp->installEventFilter(this);
22 136 : connect(m_toggle, &QAction::toggled, this, [this] { m_peeking = false; });
23 917 : for (QAction *entry : m_window->menuBar()->actions()) {
24 524 : if (QMenu *menu = entry->menu()) {
25 524 : connect(menu, &QMenu::aboutToHide, this,
26 1048 : &MenuBarPeek::hideWhenNoMenuIsOpen);
27 : }
28 : }
29 131 : }
30 :
31 0 : auto MenuBarPeek::isPeeking() const -> bool { return m_peeking; }
32 :
33 110112 : auto MenuBarPeek::eventFilter(QObject *watched, QEvent *event) -> bool {
34 110112 : switch (event->type()) {
35 20 : case QEvent::KeyPress:
36 20 : onKeyPress(watched, static_cast<QKeyEvent *>(event));
37 20 : break;
38 16 : case QEvent::KeyRelease:
39 16 : onKeyRelease(static_cast<QKeyEvent *>(event));
40 16 : break;
41 1 : case QEvent::MouseButtonPress:
42 1 : if (m_peeking && inWindow(watched) && !onMenuBar(watched)) {
43 0 : hideWhenNoMenuIsOpen();
44 : }
45 : break;
46 1112 : case QEvent::WindowDeactivate:
47 1112 : if (watched == m_window) {
48 : // Alt+Tab: the release goes to another application.
49 25 : m_altDown = false;
50 25 : hideWhenNoMenuIsOpen();
51 : }
52 : break;
53 : default:
54 : break;
55 : }
56 110112 : return false;
57 : }
58 :
59 : // A key event reaches the filter once for every widget it propagates to, and
60 : // again as auto-repeat; m_altDown makes the second sighting a no-op.
61 20 : void MenuBarPeek::onKeyPress(QObject *watched, const QKeyEvent *event) {
62 20 : if (event->key() != Qt::Key_Alt) {
63 10 : m_altAlone = false;
64 10 : if (event->key() == Qt::Key_Escape && m_peeking) {
65 1 : hideWhenNoMenuIsOpen();
66 : }
67 10 : return;
68 : }
69 10 : if (m_altDown || event->isAutoRepeat() || !inWindow(watched)) {
70 2 : return;
71 : }
72 8 : m_altDown = true;
73 8 : m_altAlone = true;
74 8 : m_shownByThisPress =
75 8 : !m_peeking && !m_toggle->isChecked() && !m_window->menuBar()->isVisible();
76 8 : if (m_shownByThisPress) {
77 5 : show();
78 : }
79 : }
80 :
81 16 : void MenuBarPeek::onKeyRelease(const QKeyEvent *event) {
82 16 : if (event->key() != Qt::Key_Alt || !m_altDown || event->isAutoRepeat()) {
83 : return;
84 : }
85 8 : m_altDown = false;
86 8 : if (!m_peeking) {
87 : return;
88 : }
89 6 : if (m_altAlone && !m_shownByThisPress) {
90 1 : hide(); // the second tap
91 5 : } else if (!m_altAlone) {
92 1 : hideWhenNoMenuIsOpen(); // Alt+something that was not a menu
93 : }
94 : }
95 :
96 8 : auto MenuBarPeek::inWindow(QObject *watched) const -> bool {
97 : const auto *widget = qobject_cast<QWidget *>(watched);
98 8 : return widget != nullptr && widget->window() == m_window;
99 : }
100 :
101 0 : auto MenuBarPeek::onMenuBar(QObject *watched) const -> bool {
102 : const auto *widget = qobject_cast<QWidget *>(watched);
103 0 : const QMenuBar *bar = m_window->menuBar();
104 0 : return widget == bar || bar->isAncestorOf(widget);
105 : }
106 :
107 5 : void MenuBarPeek::show() {
108 5 : m_peeking = true;
109 5 : m_window->menuBar()->show();
110 5 : }
111 :
112 4 : void MenuBarPeek::hide() {
113 4 : m_peeking = false;
114 4 : if (!m_toggle->isChecked()) {
115 4 : m_window->menuBar()->hide();
116 : }
117 4 : }
118 :
119 : // Deferred: a menu's aboutToHide also fires when the pointer moves on to the
120 : // next menu, and Escape is seen here before the bar leaves keyboard mode.
121 28 : void MenuBarPeek::hideWhenNoMenuIsOpen() {
122 28 : QTimer::singleShot(0, this, [this] {
123 11 : if (m_peeking && QApplication::activePopupWidget() == nullptr &&
124 3 : m_window->menuBar()->activeAction() == nullptr) {
125 3 : hide();
126 : }
127 8 : });
128 28 : }
|