You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Sandbox/Editor/ActionManager.cpp

656 lines
19 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "EditorDefs.h"
#include "ActionManager.h"
// Qt
#include <QSignalMapper>
#include <QDebug>
#include <QMenuBar>
#include <QScopedValueRollback>
// Editor
#include "MainWindow.h"
#include "QtViewPaneManager.h"
#include "ShortcutDispatcher.h"
#include "ToolbarManager.h"
static const char* const s_reserved = "Reserved"; ///< "Reserved" property used for actions that cannot be overridden.
///< (e.g. KeySequences such as Ctrl+S and Ctrl+Z for Save/Undo etc.)
static const char* const s_menuIdProperty = "MenuId"; ///< "MenuId" property used when adding top level menus to the
///< menu bar so they can be uniquely identified with FindMenu()
static const int s_invalidGuardActionId = -1;
ActionManagerExecutionGuard::ActionManagerExecutionGuard(ActionManager* actionManager, QAction* action)
: m_actionManager(actionManager)
, m_actionId(s_invalidGuardActionId)
, m_canExecute(true)
{
// both actionManager and action can be nullptr, and this is totally valid.
// the lambdas using this might be out of scope and their QPointers may have been cleared
if (actionManager && action)
{
m_actionId = action->data().toInt();
m_canExecute = actionManager->InsertActionExecuting(m_actionId);
}
}
ActionManagerExecutionGuard::ActionManagerExecutionGuard(ActionManager* actionManager, int actionId)
: m_actionManager(actionManager)
, m_actionId(actionId)
, m_canExecute(true)
{
// both actionManager and action can be nullptr, and this is totally valid. The lambdas using this might be out of scope and
// their QPointers may have been cleared
if (actionManager)
{
m_canExecute = actionManager->InsertActionExecuting(m_actionId);
}
}
ActionManagerExecutionGuard::~ActionManagerExecutionGuard()
{
// Only bother removing the action if it successfully inserted, indicated by m_canExecute.
// If during the insert, it was found that the action was already present, then there
// is no need to remove it, and doing so might cause problems because any it implies
// that something has already executed the action higher up the callstack and we don't
// want to remove the id so that future action triggers within the same callstack are prevented
// from executing.
if (m_canExecute && m_actionManager && (m_actionId != s_invalidGuardActionId))
{
m_actionManager->RemoveActionExecuting(m_actionId);
}
}
PatchedAction::PatchedAction(const QString& name, QObject* parent)
: QAction(name, parent)
{
}
bool PatchedAction::event(QEvent* ev)
{
// *Really* honor Qt::WindowShortcut. Floating dock widgets are a separate window (Qt::Window flag is set) even though they have a parent.
if (ev->type() == QEvent::Shortcut && shortcutContext() == Qt::WindowShortcut)
{
// This prevents shortcuts from firing while we're in a long running operation
// started by a shortcut
static bool reentranceLock = false;
if (reentranceLock)
{
return true;
}
QScopedValueRollback<bool> reset(reentranceLock, true);
QWidget* focusWidget = ShortcutDispatcher::focusWidget();
if (!focusWidget)
{
return QAction::event(ev);
}
for (QWidget* associatedWidget : associatedWidgets())
{
QWidget* associatedWindow = associatedWidget->window();
QWidget* focusWindow = focusWidget->window();
if (associatedWindow == focusWindow)
{
// Fair enough, we accept it.
return QAction::event(ev);
}
else if (associatedWindow && focusWindow)
{
/**
* But do allow if the focused window is actually a floating dock widget.
* For example, If Entity Outliner is floating, the gizmos (key 1, 2, 3 4) should still work.
*
* FIXME: But then why are those main toolbar actions using Qt::WindowShortcut instead of Qt::ApplicationShortcut ?
* This block goes against what the original PatchedAction fixed.
* Consider either removing it and using regular QActions, or using Qt::ApplicationShortcut
* See also LY-35177
*/
QString focusWindowName = focusWindow->objectName();
if (focusWindowName.isEmpty())
{
continue;
}
QWidget* child = associatedWindow->findChild<QWidget*>(focusWindowName);
if (child)
{
// Also accept if the focus window is a child of the associated window
return QAction::event(ev);
}
}
}
// Bug detected: Qt is propagating a shortcut with context Qt::WindowShortcut outside of window boundaries
// Consume the event instead of processing it.
qDebug() << "Discarding buggy shortcut";
return true;
}
return QAction::event(ev);
}
/////////////////////////////////////////////////////////////////////////////
// ActionWrapper
/////////////////////////////////////////////////////////////////////////////
ActionManager::ActionWrapper& ActionManager::ActionWrapper::SetMenu(DynamicMenu* menu)
{
menu->SetAction(m_action, m_actionManager);
return *this;
}
ActionManager::ActionWrapper& ActionManager::ActionWrapper::SetApplyHoverEffect()
{
// Our standard toolbar icons, when hovered on, get a white color effect.
// But for this to work we need .pngs that look good with this effect, so this only works with the standard toolbars
// and looks very ugly for other toolbars, including toolbars loaded from XML (which just show a white rectangle)
m_action->setProperty("IconHasHoverEffect", true);
return *this;
}
ActionManager::ActionWrapper& ActionManager::ActionWrapper::SetReserved()
{
m_action->setProperty("Reserved", true);
return *this;
}
/////////////////////////////////////////////////////////////////////////////
// DynamicMenu
/////////////////////////////////////////////////////////////////////////////
DynamicMenu::DynamicMenu(QObject* parent)
: QObject(parent)
, m_action(nullptr)
, m_menu(nullptr)
{
m_actionMapper = new QSignalMapper(this);
connect(m_actionMapper, SIGNAL(mapped(int)), this, SLOT(TriggerAction(int)));
}
void DynamicMenu::SetAction(QAction* action, ActionManager* am)
{
Q_ASSERT(action);
Q_ASSERT(am);
m_action = action;
m_menu = new QMenu();
m_action->setMenu(m_menu);
connect(m_menu, &QMenu::aboutToShow, this, &DynamicMenu::ShowMenu);
m_actionManager = am;
setParent(m_action);
}
void DynamicMenu::SetParentMenu(QMenu* menu, ActionManager* am)
{
Q_ASSERT(menu && !m_menu);
Q_ASSERT(!m_action);
m_menu = menu;
connect(m_menu, &QMenu::aboutToShow, this, &DynamicMenu::ShowMenu);
m_actionManager = am;
}
void DynamicMenu::AddAction(int id, QAction* action)
{
Q_ASSERT(!m_actions.contains(id));
action->setData(id);
m_actions[id] = action;
m_menu->addAction(action);
connect(action, SIGNAL(triggered()), m_actionMapper, SLOT(map()));
m_actionMapper->setMapping(action, id);
}
void DynamicMenu::AddSeparator()
{
Q_ASSERT(m_menu);
m_menu->addSeparator();
}
ActionManager::ActionWrapper DynamicMenu::AddAction(int id, const QString& name)
{
QAction* action = new PatchedAction(name, this);
AddAction(id, action);
return ActionManager::ActionWrapper(action, m_actionManager);
}
void DynamicMenu::UpdateAllActions()
{
for (auto action : m_menu->actions())
{
int id = action->data().toInt();
OnMenuUpdate(id, action);
}
}
void DynamicMenu::ShowMenu()
{
if (m_actions.isEmpty())
{
CreateMenu();
}
UpdateAllActions();
}
void DynamicMenu::TriggerAction(int id)
{
OnMenuChange(id, m_actions.value(id));
UpdateAllActions();
}
#include <QTimer>
/////////////////////////////////////////////////////////////////////////////
// ActionManager
/////////////////////////////////////////////////////////////////////////////
ActionManager::ActionManager(
MainWindow* parent, QtViewPaneManager* const qtViewPaneManager, ShortcutDispatcher* shortcutDispatcher)
: QObject(parent)
, m_mainWindow(parent)
, m_qtViewPaneManager(qtViewPaneManager)
, m_shortcutDispatcher(shortcutDispatcher)
{
m_actionMapper = new QSignalMapper(this);
connect(m_actionMapper, SIGNAL(mapped(int)), this, SLOT(ActionTriggered(int)));
connect(m_qtViewPaneManager, &QtViewPaneManager::registeredPanesChanged, this, &ActionManager::RebuildRegisteredViewPaneIds);
// KDAB_TODO: This will be used later, particularly for the toolbars
//connect(QCoreApplication::eventDispatcher(), SIGNAL(aboutToBlock()),
// this, SLOT(UpdateActions()));
// so long use a simple timer to make it work
QTimer* timer = new QTimer(this);
timer->setInterval(250);
connect(timer, &QTimer::timeout, this, &ActionManager::UpdateActions);
timer->start();
// connect to the Action Request Bus and notify other listeners this has happened
AzToolsFramework::EditorActionRequestBus::Handler::BusConnect();
}
ActionManager::~ActionManager()
{
AzToolsFramework::EditorActionRequestBus::Handler::BusDisconnect();
}
void ActionManager::AddMenu(QMenu* menu)
{
m_menus.push_back(menu);
connect(menu, &QMenu::aboutToShow, this, &ActionManager::UpdateMenu);
}
ActionManager::MenuWrapper ActionManager::AddMenu(const QString& title, const QString& menuId)
{
const auto menu = new QMenu(title);
// set a unique identifier for this menu item so it
// can be looked up later using FindMenu()
if (!menuId.isEmpty())
{
menu->setProperty(s_menuIdProperty, menuId);
}
AddMenu(menu);
return MenuWrapper(menu, this);
}
ActionManager::MenuWrapper ActionManager::FindMenu(const QString& menuId)
{
// attempt to find menu by menuId
auto menuIt = AZStd::find_if(m_menus.begin(), m_menus.end(), [&menuId](const QMenu* menu)
{
if (!menu->property(s_menuIdProperty).isNull())
{
return menu->property(s_menuIdProperty).toString() == menuId;
}
return false;
});
// return the menu with the matching name, if not found return nullptr
QMenu* menu = [this, menuIt, &menuId]() -> QMenu*
{
if (menuIt != m_menus.end())
{
return *menuIt;
}
AZ_Warning("ActionManager", false, "Did not find menu with menuId %s", menuId.toUtf8().data());
return nullptr;
}();
return MenuWrapper(menu, this);
}
void ActionManager::AddToolBar(QToolBar* toolBar)
{
m_toolBars.push_back(toolBar);
}
ActionManager::ToolBarWrapper ActionManager::AddToolBar(int id)
{
AmazonToolbar t = m_mainWindow->GetToolbarManager()->GetToolbar(id);
Q_ASSERT(t.IsInstantiated());
AddToolBar(t.Toolbar());
return ToolBarWrapper(t.Toolbar(), this);
}
bool ActionManager::eventFilter([[maybe_unused]] QObject* watched, QEvent* event)
{
// if events are shortcut events, we don't want to filter out
if (event->type() == QEvent::Shortcut)
{
m_isShortcutEvent = true;
}
return false;
}
bool ActionManager::InsertActionExecuting(int id)
{
// If the action handler puts up a modal dialog, the event queue will be pumped
// and double clicks on menu items will go through, in some cases.
// This is to guard against that.
if (m_executingIds.find(id) != m_executingIds.end())
{
return false;
}
m_executingIds.insert(id);
return true;
}
bool ActionManager::RemoveActionExecuting(int id)
{
bool idWasInList = m_executingIds.remove(id);
Q_ASSERT(idWasInList);
return idWasInList;
}
void ActionManager::AddAction(int id, QAction* action)
{
action->setData(id);
AddAction(action);
}
void ActionManager::AddAction(QAction* action)
{
const int id = action->data().toInt();
if (m_actions.contains(id))
{
qWarning() << "ActionManager already contains action with id=" << id;
Q_ASSERT(false);
}
m_actions[id] = action;
connect(action, SIGNAL(triggered()), m_actionMapper, SLOT(map()));
m_actionMapper->setMapping(action, id);
action->installEventFilter(this);
// Add the action if the parent is a widget
auto widget = qobject_cast<QWidget*>(parent());
if (widget)
{
widget->addAction(action);
}
}
void ActionManager::RemoveAction(QAction* action)
{
auto storedAction = m_actions.find(action->data().toInt());
if (storedAction != m_actions.end())
{
m_actions.remove(action->data().toInt());
}
action->removeEventFilter(this);
m_actionMapper->removeMappings(action);
if (auto widget = qobject_cast<QWidget*>(parent()))
{
widget->removeAction(action);
}
}
ActionManager::ActionWrapper ActionManager::AddAction(int id, const QString& name)
{
QAction* action = ActionIsWidget(id) ? new WidgetAction(id, m_mainWindow, name, this)
: static_cast<QAction*>(new PatchedAction(name, this)); // static cast to base so ternary compiles
AddAction(id, action);
return ActionWrapper(action, this);
}
bool ActionManager::HasAction(QAction* action) const
{
return action && HasAction(action->data().toInt());
}
bool ActionManager::HasAction(int id) const
{
return m_actions.contains(id);
}
QAction* ActionManager::GetAction(int id) const
{
auto it = m_actions.find(id);
if (it == m_actions.cend())
{
qWarning() << Q_FUNC_INFO << "Couldn't get action " << id;
Q_ASSERT(false);
return nullptr;
}
else
{
return *it;
}
}
void ActionManager::ActionTriggered(int id)
{
if (m_mainWindow->menuBar()->isEnabled())
{
if (m_actionHandlers.contains(id))
{
ActionManagerExecutionGuard guard(this, id);
if (guard.CanExecute())
{
m_actionHandlers[id]();
}
}
}
}
void ActionManager::RebuildRegisteredViewPaneIds()
{
QtViewPanes views = QtViewPaneManager::instance()->GetRegisteredPanes();
for (auto& view : views)
{
m_registeredViewPaneIds.insert(view.m_id);
}
}
// is this action suspended (allowed to respond or not)
static bool ActionSuspended(const bool defaultActionsSuspended, const QAction* const action)
{
// if default actions are suspended and this is not a reserved action, do not update
return defaultActionsSuspended && !action->property(s_reserved).toBool();
}
// for the menu that is about to be opened, visit each action (menu item) and
// update callbacks for unsuspended actions.
// recurse one level deep if the action is itself a menu, if all actions
// in that menu are suspended, gray out the menu so it cannot be selected.
static void UpdateMenus(
QMenu* menu, const bool defaultActionsSuspended,
QHash<int, std::function<void()>>& updateCallbacks,
QList<QAction*> topLevelMenuActions, int depth)
{
const auto actions = menu->actions();
const int actionCount = actions.size();
int suspendedActionCounter = 0;
for (auto action : actions)
{
// if an action is itself a menu, we want to check its
// own menu actions (children), but only one level down
if (action->menu() && depth == 0)
{
UpdateMenus(
action->menu(), defaultActionsSuspended,
updateCallbacks, topLevelMenuActions, depth + 1);
}
if (ActionSuspended(defaultActionsSuspended, action))
{
suspendedActionCounter++;
continue;
}
// call all update callbacks for the given menu
// only do this at the level of the menu we clicked/hovered
const auto id = action->data().toInt();
if (updateCallbacks.contains(id) && depth == 0)
{
updateCallbacks.value(id)();
}
}
// check if we are a top level menu action
if (AZStd::find(
topLevelMenuActions.begin(), topLevelMenuActions.end(),
menu->menuAction()) == topLevelMenuActions.end())
{
// if we're not a top level menu action, we want to disable
// the sub menu if none of the child actions are active, otherwise
// ensure the menu is returned to an enabled state
menu->menuAction()->setEnabled(
defaultActionsSuspended
? suspendedActionCounter != actionCount
: true);
}
}
void ActionManager::UpdateMenu()
{
auto menu = qobject_cast<QMenu*>(sender());
AZ_Assert(menu, "sender() was not convertible to a QMenu*");
int depth = 0;
UpdateMenus(
menu, m_defaultActionsSuspended, m_updateCallbacks,
m_mainWindow->menuBar()->actions(), depth);
}
void ActionManager::UpdateActions()
{
for (auto it = m_updateCallbacks.constBegin(); it != m_updateCallbacks.constEnd(); ++it)
{
if (ActionSuspended(m_defaultActionsSuspended, GetAction(it.key())))
{
continue;
}
it.value()();
}
}
QList<QAction*> ActionManager::GetActions() const
{
return m_actions.values();
}
bool ActionManager::ActionIsWidget(int id) const
{
return id >= ID_TOOLBAR_WIDGET_FIRST && id <= ID_TOOLBAR_WIDGET_LAST;
}
// either enable or disable all registered actions
void SetDefaultActionsEnabled(
const QList<QAction*>& actions, const bool enabled)
{
AZStd::for_each(
actions.begin(), actions.end(),
[enabled](QAction* action)
{
if (!action->property(s_reserved).toBool())
{
action->setEnabled(enabled);
}
});
}
void ActionManager::AddActionViaBus(int id, QAction* action)
{
AddAction(id, action);
}
void ActionManager::RemoveActionViaBus(QAction* action)
{
RemoveAction(action);
}
void ActionManager::EnableDefaultActions()
{
SetDefaultActionsEnabled(GetActions(), true);
m_defaultActionsSuspended = false;
}
void ActionManager::DisableDefaultActions()
{
SetDefaultActionsEnabled(GetActions(), false);
m_defaultActionsSuspended = true;
}
void ActionManager::AttachOverride(QWidget* object)
{
m_shortcutDispatcher->AttachOverride(object);
}
void ActionManager::DetachOverride()
{
m_shortcutDispatcher->DetachOverride();
}
WidgetAction::WidgetAction(int actionId, MainWindow* mainWindow, const QString& name, QObject* parent)
: QWidgetAction(parent)
, m_actionId(actionId)
, m_mainWindow(mainWindow)
{
setText(name);
}
QWidget* WidgetAction::createWidget(QWidget* parent)
{
QWidget* w = m_mainWindow->CreateToolbarWidget(m_actionId);
if (w)
{
w->setParent(parent);
}
return w;
}
#include <moc_ActionManager.cpp>