|
@@ -1,491 +0,0 @@
|
|
|
-/*
|
|
|
- * Copyright (c) Contributors to the Open 3D Engine Project.
|
|
|
- * For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
|
- *
|
|
|
- * SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
|
- *
|
|
|
- */
|
|
|
-
|
|
|
-#include "EditorDefs.h"
|
|
|
-
|
|
|
-#include "ShortcutDispatcher.h"
|
|
|
-
|
|
|
-#include <assert.h>
|
|
|
-
|
|
|
-
|
|
|
-// Qt
|
|
|
-#include <QScopedValueRollback>
|
|
|
-#include <QAction>
|
|
|
-#include <QDockWidget>
|
|
|
-#include <QDebug>
|
|
|
-#include <QMainWindow>
|
|
|
-#include <QMenuBar>
|
|
|
-
|
|
|
-// AzQtComponents
|
|
|
-#include <AzQtComponents/Buses/ShortcutDispatch.h>
|
|
|
-
|
|
|
-// Editor
|
|
|
-#include "QtViewPaneManager.h"
|
|
|
-
|
|
|
-const char* FOCUSED_VIEW_PANE_EVENT_NAME = "FocusedViewPaneEvent"; //Sent when view panes are focused
|
|
|
-const char* FOCUSED_VIEW_PANE_ATTRIBUTE_NAME = "FocusedViewPaneName"; //Name of the current focused view pane
|
|
|
-
|
|
|
-// Build sets _DEBUG when we're in debug mode; define SHOW_ACTION_INFO_IN_DEBUGGER so that when stepping through the debugger,
|
|
|
-// we can see what the actions are and what their keyboard shortcuts are
|
|
|
-#define SHOW_ACTION_INFO_IN_DEBUGGER _DEBUG
|
|
|
-
|
|
|
-static QPointer<QWidget> s_lastFocus;
|
|
|
-
|
|
|
-#if AZ_TRAIT_OS_PLATFORM_APPLE
|
|
|
-class MacNativeShortcutFilter : public QObject
|
|
|
-{
|
|
|
- /* mac's native toolbar doesn't generate shortcut events, it calls the action directly.
|
|
|
- * It doesn't even honour shortcut contexts.
|
|
|
- *
|
|
|
- * To remedy this, we catch the QMetaCallEvent that triggers the menu item activation
|
|
|
- * and suppress it if it was triggered via key combination, and send a QShortcutEvent.
|
|
|
- *
|
|
|
- * The tricky part is to find out if the menu item was triggered via mouse or shortcut.
|
|
|
- * If the previous event was a ShortcutOverride then it means key press.
|
|
|
- */
|
|
|
-
|
|
|
-public:
|
|
|
- explicit MacNativeShortcutFilter(QObject* parent)
|
|
|
- : QObject(parent)
|
|
|
- , m_lastShortcutOverride(QEvent::KeyPress, 0, Qt::NoModifier) // dummy init
|
|
|
- {
|
|
|
- qApp->installEventFilter(this);
|
|
|
- }
|
|
|
-
|
|
|
- bool eventFilter(QObject* watched, QEvent* event) override
|
|
|
- {
|
|
|
-
|
|
|
- switch (event->type())
|
|
|
- {
|
|
|
- case QEvent::ShortcutOverride:
|
|
|
- {
|
|
|
- auto ke = static_cast<QKeyEvent*>(event);
|
|
|
- m_lastEventWasShortcutOverride = true;
|
|
|
- m_lastShortcutOverride = QKeyEvent(*ke);
|
|
|
- break;
|
|
|
- }
|
|
|
- case QEvent::MetaCall:
|
|
|
- {
|
|
|
- if (m_lastEventWasShortcutOverride)
|
|
|
- {
|
|
|
- m_lastEventWasShortcutOverride = false;
|
|
|
- const QMetaObject* mo = watched->metaObject();
|
|
|
- const bool isMenuItem = mo && qstrcmp(mo->className(), "QPlatformMenuItem") == 0;
|
|
|
- if (isMenuItem)
|
|
|
- {
|
|
|
- QWidget* focusWidget = ShortcutDispatcher::focusWidget();
|
|
|
- if (focusWidget)
|
|
|
- {
|
|
|
- QShortcutEvent se(QKeySequence(m_lastShortcutOverride.key() + m_lastShortcutOverride.modifiers()), /*ambiguous=*/false);
|
|
|
- se.setAccepted(false);
|
|
|
- QApplication::sendEvent(focusWidget, &se);
|
|
|
- return se.isAccepted();
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- break;
|
|
|
- }
|
|
|
- case QEvent::MouseButtonDblClick:
|
|
|
- case QEvent::MouseButtonPress:
|
|
|
- case QEvent::MouseButtonRelease:
|
|
|
- case QEvent::KeyPress:
|
|
|
- case QEvent::KeyRelease:
|
|
|
- m_lastEventWasShortcutOverride = false;
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- bool m_lastEventWasShortcutOverride = false;
|
|
|
- QKeyEvent m_lastShortcutOverride;
|
|
|
-};
|
|
|
-#endif
|
|
|
-
|
|
|
-// Returns either a top-level or a dock widget (regardless of floating)
|
|
|
-// This way when docking a main window Qt::WindowShortcut still works
|
|
|
-QWidget* ShortcutDispatcher::FindParentScopeRoot(QWidget* widget)
|
|
|
-{
|
|
|
- QWidget* w = widget;
|
|
|
- // if the current scope root is a QDockWidget, we want to bubble out
|
|
|
- // so we move to the parent immediately
|
|
|
- if (qobject_cast<QDockWidget*>(w) != nullptr || qobject_cast<QMainWindow*>(w) != nullptr)
|
|
|
- {
|
|
|
- w = w->parentWidget();
|
|
|
- }
|
|
|
-
|
|
|
- QWidget* newScopeRoot = w;
|
|
|
- while (newScopeRoot && newScopeRoot->parent() && !qobject_cast<QDockWidget*>(newScopeRoot) && !qobject_cast<QMainWindow*>(newScopeRoot))
|
|
|
- {
|
|
|
- newScopeRoot = newScopeRoot->parentWidget();
|
|
|
- }
|
|
|
-
|
|
|
- // This method should always return a parent scope root; if it can't find one
|
|
|
- // it returns null
|
|
|
- if (newScopeRoot == widget)
|
|
|
- {
|
|
|
- newScopeRoot = nullptr;
|
|
|
-
|
|
|
- if (w != nullptr)
|
|
|
- {
|
|
|
- // we couldn't find a valid parent; broadcast a message to see if something else wants to tell us about one
|
|
|
- AzQtComponents::ShortcutDispatchBus::EventResult(newScopeRoot, w, &AzQtComponents::ShortcutDispatchBus::Events::GetShortcutDispatchScopeRoot, w);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return newScopeRoot;
|
|
|
-}
|
|
|
-
|
|
|
-// Returns true if a widget is ancestor of another widget
|
|
|
-bool ShortcutDispatcher::IsAContainerForB(QWidget* a, QWidget* b)
|
|
|
-{
|
|
|
- if (!a || !b)
|
|
|
- {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- while (b && (a != b))
|
|
|
- {
|
|
|
- b = b->parentWidget();
|
|
|
- }
|
|
|
-
|
|
|
- return (a == b);
|
|
|
-}
|
|
|
-
|
|
|
-// Returns the list of QActions which have this specific key shortcut
|
|
|
-// Only QActions under scopeRoot are considered.
|
|
|
-QList<QAction*> ShortcutDispatcher::FindCandidateActions(QObject* scopeRoot, const QKeySequence& sequence, QSet<QObject*>& previouslyVisited, bool checkVisibility)
|
|
|
-{
|
|
|
- QList<QAction*> actions;
|
|
|
- if (!scopeRoot)
|
|
|
- {
|
|
|
- return actions;
|
|
|
- }
|
|
|
-
|
|
|
- if (previouslyVisited.contains(scopeRoot))
|
|
|
- {
|
|
|
- return actions;
|
|
|
- }
|
|
|
- previouslyVisited.insert(scopeRoot);
|
|
|
-
|
|
|
- QWidget* scopeRootWidget = qobject_cast<QWidget*>(scopeRoot);
|
|
|
- if (scopeRootWidget && ((checkVisibility && !scopeRootWidget->isVisible()) || !scopeRootWidget->isEnabled()))
|
|
|
- {
|
|
|
- return actions;
|
|
|
- }
|
|
|
-
|
|
|
-#ifdef SHOW_ACTION_INFO_IN_DEBUGGER
|
|
|
- QString matchingAgainst = sequence.toString();
|
|
|
- (void) matchingAgainst; // avoid an unused variable warning; want this for debugging
|
|
|
-#endif
|
|
|
-
|
|
|
- // Don't just call scopeRoot->actions()! It doesn't always return the proper list, especially with the dock widgets
|
|
|
- for (QAction* action : scopeRoot->findChildren<QAction*>(QString(), Qt::FindDirectChildrenOnly)) // Width first
|
|
|
- {
|
|
|
-#ifdef SHOW_ACTION_INFO_IN_DEBUGGER
|
|
|
- QString actionName = action->text();
|
|
|
- (void)actionName; // avoid an unused variable warning; want this for debugging
|
|
|
- QString shortcut = action->shortcut().toString();
|
|
|
- (void)shortcut; // avoid an unused variable warning; want this for debugging
|
|
|
-#endif
|
|
|
-
|
|
|
- if (action->shortcut() == sequence)
|
|
|
- {
|
|
|
- actions << action;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // also have to check the actions on the object directly, without looking at children
|
|
|
- // specifically for the base Editor MainWindow
|
|
|
- if (scopeRootWidget)
|
|
|
- {
|
|
|
- for (QAction* action : scopeRootWidget->actions())
|
|
|
- {
|
|
|
-#ifdef SHOW_ACTION_INFO_IN_DEBUGGER
|
|
|
- QString actionName = action->text();
|
|
|
- (void)actionName; // avoid an unused variable warning; want this for debugging
|
|
|
- QString shortcut = action->shortcut().toString();
|
|
|
- (void)shortcut; // avoid an unused variable warning; want this for debugging
|
|
|
-#endif
|
|
|
-
|
|
|
- if (action->shortcut() == sequence)
|
|
|
- {
|
|
|
- actions << action;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Menubars have child widgets that have actions
|
|
|
- // But menu bar child widgets (menu items) are only visible when they've been clicked on
|
|
|
- // so we don't want to test visibility for child widgets of menubars
|
|
|
- if (qobject_cast<QMenuBar*>(scopeRoot))
|
|
|
- {
|
|
|
- checkVisibility = false;
|
|
|
- }
|
|
|
-
|
|
|
- // check the dock's central widget and the main window's
|
|
|
- // In some cases, they aren't in the scopeRoot's children, despite having the scopeRoot as their parent
|
|
|
- QDockWidget* dockWidget = qobject_cast<QDockWidget*>(scopeRoot);
|
|
|
- if (dockWidget)
|
|
|
- {
|
|
|
- actions << FindCandidateActions(dockWidget->widget(), sequence, previouslyVisited, checkVisibility);
|
|
|
- }
|
|
|
-
|
|
|
- QMainWindow* mainWindow = qobject_cast<QMainWindow*>(scopeRoot);
|
|
|
- if (mainWindow)
|
|
|
- {
|
|
|
- actions << FindCandidateActions(mainWindow->centralWidget(), sequence, previouslyVisited, checkVisibility);
|
|
|
- }
|
|
|
-
|
|
|
- for (QWidget* child : scopeRoot->findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly))
|
|
|
- {
|
|
|
- bool isMenu = (qobject_cast<QMenu*>(child) != nullptr);
|
|
|
-
|
|
|
- if ((child->windowFlags() & Qt::Window) && !isMenu)
|
|
|
- {
|
|
|
- // When going down the hierarchy stop at window boundaries, to not accidentally trigger
|
|
|
- // shortcuts from unfocused windows. Windows might be parented to this scope for purposes
|
|
|
- // "centering within parent" or lifetime.
|
|
|
- // Don't stop at menus though, as they are flagged as Qt::Window but are often the only thing that
|
|
|
- // actions are attached to.
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- bool isDockWidget = (qobject_cast<QDockWidget*>(child) != nullptr);
|
|
|
- if ((isDockWidget && !actions.isEmpty()) || IsShortcutSearchBreak(child))
|
|
|
- {
|
|
|
- // If we already found a candidate, don't go into dock widgets, they have lower priority
|
|
|
- // since they are not focused.
|
|
|
- // Also never go into viewpanes; viewpanes are their own separate shortcut context and they never take shortcuts from the main window
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- actions << FindCandidateActions(child, sequence, previouslyVisited, checkVisibility);
|
|
|
- }
|
|
|
-
|
|
|
- return actions;
|
|
|
-}
|
|
|
-
|
|
|
-bool ShortcutDispatcher::FindCandidateActionAndFire(QObject* focusWidget, QShortcutEvent* shortcutEvent, QList<QAction*>& candidates, QSet<QObject*>& previouslyVisited)
|
|
|
-{
|
|
|
- candidates = FindCandidateActions(focusWidget, shortcutEvent->key(), previouslyVisited);
|
|
|
- QSet<QAction*> candidateSet = QSet<QAction*>(candidates.begin(), candidates.end());
|
|
|
- QAction* chosenAction = nullptr;
|
|
|
- chosenAction = candidates.first();
|
|
|
-
|
|
|
- if (chosenAction)
|
|
|
- {
|
|
|
- if (chosenAction->isEnabled())
|
|
|
- {
|
|
|
- // has to be send, not post, or the dispatcher will get the event again and won't know that it was the one that queued it
|
|
|
- bool isAmbiguous = false;
|
|
|
- QShortcutEvent newEvent(shortcutEvent->key(), isAmbiguous);
|
|
|
- QApplication::sendEvent(chosenAction, &newEvent);
|
|
|
- }
|
|
|
- shortcutEvent->accept();
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
-}
|
|
|
-
|
|
|
-ShortcutDispatcher::ShortcutDispatcher(QObject* parent)
|
|
|
- : QObject(parent)
|
|
|
- , m_currentlyHandlingShortcut(false)
|
|
|
-{
|
|
|
- qApp->installEventFilter(this);
|
|
|
-#if AZ_TRAIT_OS_PLATFORM_APPLE
|
|
|
- new MacNativeShortcutFilter(this);
|
|
|
-#endif
|
|
|
-}
|
|
|
-
|
|
|
-ShortcutDispatcher::~ShortcutDispatcher()
|
|
|
-{
|
|
|
-}
|
|
|
-
|
|
|
-bool ShortcutDispatcher::eventFilter(QObject* obj, QEvent* ev)
|
|
|
-{
|
|
|
- switch (ev->type())
|
|
|
- {
|
|
|
- case QEvent::ShortcutOverride:
|
|
|
- // QActions default "autoRepeat" to true, which is not an ideal user experience
|
|
|
- // We globally disable that behavior here - in the unlikely event a shortcut needs to
|
|
|
- // replicate it, its owner can instead implement a keyEvent handler
|
|
|
- if (static_cast<QKeyEvent*>(ev)->isAutoRepeat())
|
|
|
- {
|
|
|
- ev->accept();
|
|
|
- return true;
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case QEvent::Shortcut:
|
|
|
- return shortcutFilter(obj, static_cast<QShortcutEvent*>(ev));
|
|
|
-
|
|
|
- case QEvent::MouseButtonPress:
|
|
|
- if (!s_lastFocus || !IsAContainerForB(qobject_cast<QWidget*>(obj), s_lastFocus))
|
|
|
- {
|
|
|
- setNewFocus(obj);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case QEvent::FocusIn:
|
|
|
- setNewFocus(obj);
|
|
|
- break;
|
|
|
-
|
|
|
- // we don't really care about focus out, because something should always have the focus
|
|
|
- // but I'm leaving this here, so that it's clear that this is intentional
|
|
|
- //case QEvent::FocusOut:
|
|
|
- // break;
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
-}
|
|
|
-
|
|
|
-QWidget* ShortcutDispatcher::focusWidget()
|
|
|
-{
|
|
|
- QWidget* focusWidget = s_lastFocus; // check the widget we tracked last
|
|
|
- if (!focusWidget)
|
|
|
- {
|
|
|
- // we don't have anything, so fall back to using the focus object
|
|
|
- focusWidget = qobject_cast<QWidget*>(qApp->focusObject()); // QApplication::focusWidget() doesn't always work
|
|
|
- }
|
|
|
-
|
|
|
- return focusWidget;
|
|
|
-}
|
|
|
-
|
|
|
-bool ShortcutDispatcher::shortcutFilter(QObject* obj, QShortcutEvent* shortcutEvent)
|
|
|
-{
|
|
|
- if (m_currentlyHandlingShortcut)
|
|
|
- {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- QScopedValueRollback<bool> recursiveCheck(m_currentlyHandlingShortcut, true);
|
|
|
-
|
|
|
- // prioritize m_actionOverrideObject if active
|
|
|
- if (m_actionOverrideObject != nullptr)
|
|
|
- {
|
|
|
- QList<QAction*> childActions =
|
|
|
- m_actionOverrideObject->findChildren<QAction*>(QString(), Qt::FindDirectChildrenOnly);
|
|
|
-
|
|
|
- // attempt to find shortcut in override
|
|
|
- const auto childActionIt = AZStd::find_if(
|
|
|
- childActions.begin(), childActions.end(), [shortcutEvent](QAction* child)
|
|
|
- {
|
|
|
- return child->shortcut() == shortcutEvent->key();
|
|
|
- });
|
|
|
-
|
|
|
- // trigger shortcut
|
|
|
- if (childActionIt != childActions.end())
|
|
|
- {
|
|
|
- // has to be send, not post, or the dispatcher will get the event again
|
|
|
- // and won't know that it was the one that queued it
|
|
|
- const bool isAmbiguous = false;
|
|
|
- QShortcutEvent newEvent(shortcutEvent->key(), isAmbiguous);
|
|
|
-
|
|
|
- QAction* action = *childActionIt;
|
|
|
- QApplication::sendEvent(action, &newEvent);
|
|
|
-
|
|
|
- shortcutEvent->accept();
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- QWidget* currentFocusWidget = focusWidget(); // check the widget we tracked last
|
|
|
- if (!currentFocusWidget)
|
|
|
- {
|
|
|
- qWarning() << Q_FUNC_INFO << "No focus widget"; // Defensive. Doesn't happen.
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // Shortcut is ambiguous, lets resolve ambiguity and give preference to QActions in the most inner scope
|
|
|
-
|
|
|
- // Try below the focusWidget first:
|
|
|
- QSet<QObject*> previouslyVisited;
|
|
|
- QList<QAction*> candidates;
|
|
|
- if (FindCandidateActionAndFire(currentFocusWidget, shortcutEvent, candidates, previouslyVisited)) {
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // Now incrementally try bigger scopes. This handles complex cases several levels docking nesting
|
|
|
-
|
|
|
- QWidget* correctedTopLevel = nullptr;
|
|
|
- QWidget* p = currentFocusWidget;
|
|
|
- correctedTopLevel = FindParentScopeRoot(p);
|
|
|
- while (correctedTopLevel)
|
|
|
- {
|
|
|
- if (FindCandidateActionAndFire(correctedTopLevel, shortcutEvent, candidates, previouslyVisited))
|
|
|
- {
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- p = correctedTopLevel;
|
|
|
- correctedTopLevel = FindParentScopeRoot(p);
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- // Nothing else to do... shortcut is really ambiguous, or there's no actions, something for the developer to fix.
|
|
|
- // Here's some debug info :
|
|
|
-
|
|
|
- if (candidates.isEmpty())
|
|
|
- {
|
|
|
- qWarning() << Q_FUNC_INFO << "No candidate QActions found";
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- qWarning() << Q_FUNC_INFO << "Ambiguous shortcut:" << shortcutEvent->key() << "; focusWidget="
|
|
|
- << qApp->focusWidget() << "Candidates=" << candidates << "; obj = " << obj
|
|
|
- << "Focused top-level=" << currentFocusWidget;
|
|
|
- for (auto ambiguousAction : candidates)
|
|
|
- {
|
|
|
- qWarning() << "action=" << ambiguousAction << "; action->parentWidget=" << ambiguousAction->parentWidget()
|
|
|
- << "; associatedWidgets=" << ambiguousAction->associatedWidgets()
|
|
|
- << "; shortcut=" << ambiguousAction->shortcut();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
-}
|
|
|
-
|
|
|
-void ShortcutDispatcher::setNewFocus(QObject* obj)
|
|
|
-{
|
|
|
- // Unless every widget has strong focus, mouse clicks don't change the current focus widget
|
|
|
- // which is a little unintuitive, compared to how we expect focus to work, right now.
|
|
|
- // So instead of putting strong focus on everything, we detect focus change and mouse clicks
|
|
|
-
|
|
|
- QWidget* widget = qobject_cast<QWidget*>(obj);
|
|
|
-
|
|
|
- // we only watch widgets
|
|
|
- if (widget == nullptr)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // track it for later
|
|
|
- s_lastFocus = widget;
|
|
|
-}
|
|
|
-
|
|
|
-bool ShortcutDispatcher::IsShortcutSearchBreak(QWidget* widget)
|
|
|
-{
|
|
|
- return widget->property(AzQtComponents::SHORTCUT_DISPATCHER_CONTEXT_BREAK_PROPERTY).toBool();
|
|
|
-}
|
|
|
-
|
|
|
-void ShortcutDispatcher::AttachOverride(QWidget* object)
|
|
|
-{
|
|
|
- m_actionOverrideObject = object;
|
|
|
-}
|
|
|
-
|
|
|
-void ShortcutDispatcher::DetachOverride()
|
|
|
-{
|
|
|
- m_actionOverrideObject = nullptr;
|
|
|
-}
|
|
|
-
|
|
|
-#include <moc_ShortcutDispatcher.cpp>
|