Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "navigableappmenumodel.h"
23 :
24 : #include <QGuiApplication>
25 : #include <QWindow>
26 : #include <QKeyEvent>
27 :
28 : #include <private/qkeymapper_p.h>
29 :
30 : #include "ui/navigationcommands.h"
31 :
32 : #include "log.h"
33 :
34 : using namespace mu::appshell;
35 : using namespace muse::ui;
36 : using namespace muse::uicomponents;
37 :
38 0 : QSet<int> convertToSet(QList<int> keys)
39 : {
40 0 : return QSet<int>(keys.cbegin(), keys.cend());
41 : }
42 :
43 0 : QSet<int> convertToSet(QList<QKeyCombination> keys)
44 : {
45 0 : QSet<int> keyset;
46 0 : for (const auto& key : keys) {
47 0 : keyset << key.toCombined();
48 : }
49 0 : return keyset;
50 0 : }
51 :
52 0 : QSet<int> possibleKeys(QKeyEvent* keyEvent)
53 : {
54 0 : QKeyEvent* correctedKeyEvent = keyEvent;
55 : //! NOTE: correct work only with alt modifier
56 0 : correctedKeyEvent->setModifiers(Qt::AltModifier);
57 :
58 0 : auto keys = QKeyMapper::possibleKeys(correctedKeyEvent);
59 0 : return convertToSet(keys);
60 0 : }
61 :
62 0 : QSet<int> possibleKeys(const QChar& keySymbol)
63 : {
64 0 : QKeyEvent fakeKey(QKeyEvent::KeyRelease, Qt::Key_unknown, Qt::AltModifier, keySymbol);
65 0 : auto keys = QKeyMapper::possibleKeys(&fakeKey);
66 :
67 0 : return convertToSet(keys);
68 0 : }
69 :
70 0 : NavigableAppMenuModel::NavigableAppMenuModel(QObject* parent)
71 0 : : AppMenuModel(parent)
72 : {
73 0 : }
74 :
75 0 : void NavigableAppMenuModel::load()
76 : {
77 0 : AppMenuModel::load();
78 :
79 0 : connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state){
80 0 : if (state != Qt::ApplicationActive) {
81 0 : resetNavigation();
82 : }
83 0 : });
84 :
85 0 : navigationController()->navigationChanged().onNotify(this, [this](){
86 0 : if (navigationController()->isHighlight() && !isMenuOpened()) {
87 0 : resetNavigation();
88 : }
89 0 : });
90 :
91 0 : qApp->installEventFilter(this);
92 0 : }
93 :
94 0 : void NavigableAppMenuModel::handleMenuItem(const QString& itemId)
95 : {
96 0 : resetNavigation();
97 0 : restoreMUNavigationSystemState();
98 :
99 0 : AppMenuModel::handleMenuItem(itemId);
100 0 : }
101 :
102 0 : void NavigableAppMenuModel::openPrevMenu()
103 : {
104 0 : navigate(Qt::Key_Left);
105 0 : activateHighlightedMenu();
106 0 : }
107 :
108 0 : void NavigableAppMenuModel::openNextMenu()
109 : {
110 0 : navigate(Qt::Key_Right);
111 0 : activateHighlightedMenu();
112 0 : }
113 :
114 0 : void NavigableAppMenuModel::openMenu(const QString& menuId, bool byHover)
115 : {
116 0 : bool navigationStarted = isNavigationStarted();
117 0 : bool menuIsAlreadyOpened = m_openedMenuId == menuId;
118 0 : if (!byHover && !menuIsAlreadyOpened) {
119 0 : if (navigationStarted || !isMenuOpened()) {
120 0 : saveMUNavigationSystemState();
121 : } else {
122 0 : restoreMUNavigationSystemState();
123 : }
124 : }
125 :
126 0 : if (navigationStarted) {
127 0 : setHighlightedMenuId(menuId);
128 : }
129 :
130 0 : emit openMenuRequested(menuId, byHover);
131 0 : }
132 :
133 0 : bool NavigableAppMenuModel::isNavigationStarted() const
134 : {
135 0 : return !m_highlightedMenuId.isEmpty();
136 : }
137 :
138 0 : bool NavigableAppMenuModel::isMenuOpened() const
139 : {
140 0 : return !m_openedMenuId.isEmpty();
141 : }
142 :
143 0 : QWindow* NavigableAppMenuModel::appWindow() const
144 : {
145 0 : return m_appWindow;
146 : }
147 :
148 0 : void NavigableAppMenuModel::setAppWindow(QWindow* appWindow)
149 : {
150 0 : m_appWindow = appWindow;
151 0 : }
152 :
153 0 : void NavigableAppMenuModel::setHighlightedMenuId(QString highlightedMenuId)
154 : {
155 0 : if (m_highlightedMenuId == highlightedMenuId) {
156 0 : return;
157 : }
158 :
159 0 : m_highlightedMenuId = highlightedMenuId;
160 0 : emit highlightedMenuIdChanged(m_highlightedMenuId);
161 : }
162 :
163 0 : void NavigableAppMenuModel::setOpenedMenuId(QString openedMenuId)
164 : {
165 0 : if (m_openedMenuId == openedMenuId) {
166 0 : return;
167 : }
168 :
169 0 : m_openedMenuId = openedMenuId;
170 :
171 0 : bool navigationStarted = isNavigationStarted();
172 0 : bool menuOpened = isMenuOpened();
173 :
174 : //! NOTE: When the user navigates through the menu, MU navigation is highlighted.
175 : //! Reset the highlighted state after the menu is closed
176 0 : if (navigationStarted) {
177 0 : navigationController()->setIsHighlight(false);
178 : }
179 :
180 : //! NOTE: user closed menu by mouse
181 0 : if (!navigationStarted && !menuOpened) {
182 0 : restoreMUNavigationSystemState();
183 : }
184 :
185 : //! NOTE: after opening the menu, position on the first control
186 0 : if (navigationStarted && menuOpened) {
187 0 : commandDispatcher()->dispatch(FIRST_CONTROL_COMMAND);
188 : }
189 :
190 0 : emit openedMenuIdChanged(m_openedMenuId);
191 : }
192 :
193 0 : void NavigableAppMenuModel::setAppMenuAreaRect(QRect appMenuAreaRect)
194 : {
195 0 : if (m_appMenuAreaRect == appMenuAreaRect) {
196 0 : return;
197 : }
198 :
199 0 : m_appMenuAreaRect = appMenuAreaRect;
200 0 : emit appMenuAreaRectChanged(m_appMenuAreaRect);
201 : }
202 :
203 0 : void NavigableAppMenuModel::setOpenedMenuAreaRect(QRect openedMenuAreaRect)
204 : {
205 0 : if (m_openedMenuAreaRect == openedMenuAreaRect) {
206 0 : return;
207 : }
208 :
209 0 : m_openedMenuAreaRect = openedMenuAreaRect;
210 0 : emit openedMenuAreaRectChanged(m_openedMenuAreaRect);
211 : }
212 :
213 0 : bool NavigableAppMenuModel::eventFilter(QObject* watched, QEvent* event)
214 : {
215 0 : bool isMenuOpened = !m_openedMenuId.isEmpty();
216 0 : if (event->type() == QEvent::MouseButtonPress && watched == appWindow()) {
217 0 : QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
218 0 : bool clickOutsideAppMenuAreaRect = !m_appMenuAreaRect.contains(mouseEvent->pos());
219 0 : bool clickOutsideOpenedMenu = isMenuOpened ? !m_openedMenuAreaRect.contains(mouseEvent->pos()) : true;
220 0 : if (clickOutsideAppMenuAreaRect && clickOutsideOpenedMenu) {
221 0 : resetNavigation();
222 0 : emit closeOpenedMenuRequested();
223 0 : return false;
224 : }
225 : }
226 :
227 0 : if (isMenuOpened && watched && watched->isWindowType()) {
228 0 : return processEventForOpenedMenu(event);
229 : }
230 :
231 0 : if (watched == appWindow()) {
232 0 : bool ok = processEventForAppMenu(event);
233 0 : if (ok) {
234 0 : return ok;
235 : }
236 : }
237 :
238 0 : return AbstractMenuModel::eventFilter(watched, event);
239 : }
240 :
241 0 : bool NavigableAppMenuModel::processEventForOpenedMenu(QEvent* event)
242 : {
243 0 : if (event->type() != QEvent::ShortcutOverride) {
244 0 : return false;
245 : }
246 :
247 0 : QKeyEvent* keyEvent = dynamic_cast<QKeyEvent*>(event);
248 :
249 0 : bool isNavigationWithSymbol = !keyEvent->modifiers()
250 0 : && keyEvent->text().length() == 1;
251 :
252 0 : if (!isNavigationWithSymbol || isNavigateKey(keyEvent->key())) {
253 0 : return false;
254 : }
255 :
256 0 : QChar symbol = keyEvent->text()[0];
257 0 : emit navigateWithSymbolRequested(symbol);
258 0 : event->accept();
259 0 : return true;
260 : }
261 :
262 0 : bool NavigableAppMenuModel::processEventForAppMenu(QEvent* event)
263 : {
264 0 : QKeyEvent* keyEvent = dynamic_cast<QKeyEvent*>(event);
265 0 : if (!keyEvent) {
266 0 : return false;
267 : }
268 :
269 0 : Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
270 0 : int key = keyEvent->key();
271 0 : bool isSingleSymbol = keyEvent->text().length() == 1;
272 :
273 0 : bool isNavigationStarted = this->isNavigationStarted();
274 0 : bool isNavigationWithSymbol = !modifiers
275 0 : && isSingleSymbol
276 0 : && isNavigationStarted;
277 0 : bool isNavigationWithAlt = (modifiers == Qt::AltModifier)
278 0 : && isSingleSymbol;
279 :
280 : bool isAltKey = key == Qt::Key_Alt
281 0 : && !(modifiers & Qt::ControlModifier)
282 0 : && !(modifiers & Qt::ShiftModifier);
283 :
284 0 : switch (event->type()) {
285 0 : case QEvent::ShortcutOverride: {
286 0 : if (isNavigationStarted && isNavigateKey(key)) {
287 0 : event->accept();
288 0 : return true;
289 0 : } else if (isNavigationWithSymbol || isNavigationWithAlt) {
290 0 : QSet<int> activatePossibleKeys = possibleKeys(keyEvent);
291 0 : if (hasItem(activatePossibleKeys)) {
292 0 : event->accept();
293 0 : return true;
294 : }
295 0 : }
296 :
297 0 : m_needActivateHighlight = false;
298 :
299 0 : break;
300 : }
301 0 : case QEvent::KeyPress: {
302 0 : if (isAltKey) {
303 0 : m_needActivateHighlight = true;
304 0 : break;
305 : }
306 :
307 0 : m_needActivateHighlight = false;
308 :
309 0 : if (isNavigationStarted && isNavigateKey(key)) {
310 0 : navigate(key);
311 :
312 0 : event->accept();
313 0 : return true;
314 0 : } else if (isNavigationWithSymbol || isNavigationWithAlt) {
315 0 : QSet<int> activatePossibleKeys = possibleKeys(keyEvent);
316 0 : if (hasItem(activatePossibleKeys)) {
317 0 : navigate(activatePossibleKeys);
318 :
319 0 : event->accept();
320 0 : return true;
321 : }
322 0 : }
323 :
324 0 : break;
325 : }
326 0 : case QEvent::KeyRelease: {
327 0 : if (!isAltKey) {
328 0 : break;
329 : }
330 :
331 0 : if (isNavigationStarted) {
332 0 : resetNavigation();
333 0 : restoreMUNavigationSystemState();
334 : } else {
335 0 : if (m_needActivateHighlight) {
336 0 : m_needActivateHighlight = false;
337 0 : saveMUNavigationSystemState();
338 0 : navigateToFirstMenu();
339 : }
340 : }
341 :
342 0 : event->accept();
343 0 : return true;
344 : }
345 0 : case QEvent::MouseButtonPress: {
346 0 : resetNavigation();
347 0 : break;
348 : }
349 0 : default:
350 0 : break;
351 : }
352 0 : return false;
353 : }
354 :
355 0 : bool NavigableAppMenuModel::isNavigateKey(int key) const
356 : {
357 : static const QList<Qt::Key> keys {
358 : Qt::Key_Left,
359 : Qt::Key_Right,
360 : Qt::Key_Down,
361 : Qt::Key_Space,
362 : Qt::Key_Escape,
363 : Qt::Key_Return,
364 : Qt::Key_Enter
365 0 : };
366 :
367 0 : return keys.contains(static_cast<Qt::Key>(key));
368 : }
369 :
370 0 : void NavigableAppMenuModel::navigate(int scanCode)
371 : {
372 0 : Qt::Key _key = static_cast<Qt::Key>(scanCode);
373 0 : switch (_key) {
374 0 : case Qt::Key_Left: {
375 0 : int newIndex = itemIndex(m_highlightedMenuId) - 1;
376 0 : if (newIndex < 0) {
377 0 : newIndex = rowCount() - 1;
378 : }
379 :
380 0 : setHighlightedMenuId(item(newIndex).id());
381 0 : break;
382 : }
383 0 : case Qt::Key_Right: {
384 0 : int newIndex = itemIndex(m_highlightedMenuId) + 1;
385 0 : if (newIndex > rowCount() - 1) {
386 0 : newIndex = 0;
387 : }
388 :
389 0 : setHighlightedMenuId(item(newIndex).id());
390 0 : break;
391 : }
392 0 : case Qt::Key_Down:
393 : case Qt::Key_Space:
394 : case Qt::Key_Return:
395 : case Qt::Key_Enter:
396 0 : activateHighlightedMenu();
397 0 : break;
398 0 : case Qt::Key_Escape:
399 0 : resetNavigation();
400 0 : restoreMUNavigationSystemState();
401 0 : break;
402 0 : default:
403 0 : break;
404 : }
405 0 : }
406 :
407 0 : bool NavigableAppMenuModel::hasItem(const QSet<int>& activatePossibleKeys)
408 : {
409 0 : return !menuItemId(items(), activatePossibleKeys).isEmpty();
410 : }
411 :
412 0 : void NavigableAppMenuModel::navigate(const QSet<int>& activatePossibleKeys)
413 : {
414 0 : saveMUNavigationSystemState();
415 :
416 0 : setHighlightedMenuId(menuItemId(items(), activatePossibleKeys));
417 0 : activateHighlightedMenu();
418 0 : }
419 :
420 0 : void NavigableAppMenuModel::resetNavigation()
421 : {
422 0 : setHighlightedMenuId("");
423 0 : m_needActivateHighlight = false;
424 0 : }
425 :
426 0 : void NavigableAppMenuModel::navigateToFirstMenu()
427 : {
428 0 : setHighlightedMenuId(item(0).id());
429 0 : }
430 :
431 0 : QRect NavigableAppMenuModel::appMenuAreaRect() const
432 : {
433 0 : return m_appMenuAreaRect;
434 : }
435 :
436 0 : QRect NavigableAppMenuModel::openedMenuAreaRect() const
437 : {
438 0 : return m_openedMenuAreaRect;
439 : }
440 :
441 0 : void NavigableAppMenuModel::saveMUNavigationSystemState()
442 : {
443 0 : if (m_lastActiveMUNavigationState.has_value()) {
444 0 : return;
445 : }
446 :
447 0 : bool muNavigationIsHighlight = navigationController()->isHighlight();
448 0 : m_needActivateLastMUNavigationControl = muNavigationIsHighlight;
449 :
450 0 : INavigationSection* section = navigationController()->activeSection();
451 0 : INavigationPanel* panel = navigationController()->activePanel();
452 0 : INavigationControl* control = navigationController()->activeControl();
453 : m_lastActiveMUNavigationState = {
454 0 : section ? section->name().toStdString() : "",
455 0 : panel ? panel->name().toStdString() : "",
456 0 : control ? control->name().toStdString() : ""
457 0 : };
458 :
459 0 : if (control) {
460 0 : control->setActive(false);
461 : }
462 0 : }
463 :
464 0 : void NavigableAppMenuModel::restoreMUNavigationSystemState()
465 : {
466 0 : if (m_lastActiveMUNavigationState.has_value()) {
467 0 : MUNavigationSystemState state = m_lastActiveMUNavigationState.value();
468 :
469 0 : bool ok = navigationController()->requestActivateByName(state.sectionName, state.panelName, state.controlName);
470 0 : if (!ok) {
471 0 : navigationController()->resetNavigation();
472 : }
473 :
474 0 : m_lastActiveMUNavigationState.reset();
475 0 : }
476 :
477 0 : navigationController()->setIsHighlight(m_needActivateLastMUNavigationControl);
478 0 : }
479 :
480 0 : void NavigableAppMenuModel::activateHighlightedMenu()
481 : {
482 0 : emit openMenuRequested(m_highlightedMenuId, false);
483 0 : }
484 :
485 0 : QString NavigableAppMenuModel::highlightedMenuId() const
486 : {
487 0 : return m_highlightedMenuId;
488 : }
489 :
490 0 : QString NavigableAppMenuModel::openedMenuId() const
491 : {
492 0 : return m_openedMenuId;
493 : }
494 :
495 0 : bool NavigableAppMenuModel::menuItemMatchesSymbol(MenuItem* menuItem, const QChar& symbol)
496 : {
497 0 : QString title = menuItem->titleWithMnemonicAmpersand();
498 :
499 0 : int activateKeyIndex = title.indexOf('&');
500 0 : if (activateKeyIndex == -1) {
501 0 : return false;
502 : }
503 :
504 0 : auto keys = possibleKeys(symbol.toUpper());
505 0 : auto menuActivatePossibleKeys = possibleKeys(title[activateKeyIndex + 1].toUpper());
506 0 : if (menuActivatePossibleKeys.intersects(keys)) {
507 0 : return true;
508 : }
509 :
510 0 : return false;
511 0 : }
512 :
513 0 : QString NavigableAppMenuModel::menuItemId(const MenuItemList& items, const QSet<int>& activatePossibleKeys)
514 : {
515 0 : for (const MenuItem* item : items) {
516 0 : QString title = item->titleWithMnemonicAmpersand();
517 :
518 0 : int activateKeyIndex = title.indexOf('&');
519 0 : if (activateKeyIndex == -1) {
520 0 : continue;
521 : }
522 :
523 0 : auto menuActivatePossibleKeys = possibleKeys(title[activateKeyIndex + 1].toUpper());
524 0 : if (menuActivatePossibleKeys.intersects(activatePossibleKeys)) {
525 0 : return item->id();
526 : }
527 0 : }
528 :
529 0 : return QString();
530 : }
|