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/Editor/Core/QtEditorApplication.cpp

656 lines
22 KiB
C++

/*
* 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 "QtEditorApplication.h"
// Qt
#include <QAbstractEventDispatcher>
#include <QScopedValueRollback>
#include <QToolBar>
#include <QLoggingCategory>
#if defined(AZ_PLATFORM_WINDOWS)
#include <QtGui/qpa/qplatformnativeinterface.h>
#include <QtGui/private/qhighdpiscaling_p.h>
#endif
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
// AzFramework
#if defined(AZ_PLATFORM_WINDOWS)
# include <AzFramework/Input/Buses/Notifications/RawInputNotificationBus_Platform.h>
#endif // defined(AZ_PLATFORM_WINDOWS)
// AzQtComponents
#include <AzQtComponents/Components/GlobalEventFilter.h>
#include <AzQtComponents/Components/O3DEStylesheet.h>
#include <AzQtComponents/Components/Titlebar.h>
#include <AzQtComponents/Components/WindowDecorationWrapper.h>
// Editor
#include "Settings.h"
#include "CryEdit.h"
enum
{
// in milliseconds
GameModeIdleFrequency = 0,
EditorModeIdleFrequency = 1,
InactiveModeFrequency = 10,
UninitializedFrequency = 9999,
};
Q_LOGGING_CATEGORY(InputDebugging, "o3de.editor.input")
// internal, private namespace:
namespace
{
class EditorGlobalEventFilter
: public AzQtComponents::GlobalEventFilter
{
public:
explicit EditorGlobalEventFilter(QObject* watch)
: AzQtComponents::GlobalEventFilter(watch) {}
bool eventFilter(QObject* obj, QEvent* e) override
{
static bool isRecursing = false;
if (isRecursing)
{
return false;
}
QScopedValueRollback<bool> guard(isRecursing, true);
// Detect Widget move
// We're doing this before the events are actually consumed to avoid confusion
if (IsDragGuardedWidget(obj))
{
switch (e->type())
{
case QEvent::MouseButtonPress:
{
m_widgetDraggedState = WidgetDraggedState::Clicked;
break;
}
case QEvent::Move:
case QEvent::MouseMove:
{
if (m_widgetDraggedState == WidgetDraggedState::Clicked)
{
m_widgetDraggedState = WidgetDraggedState::Dragged;
}
break;
}
}
}
if (e->type() == QEvent::MouseButtonRelease)
{
m_widgetDraggedState = WidgetDraggedState::None;
}
switch (e->type())
{
case QEvent::KeyPress:
case QEvent::KeyRelease:
{
if (GetIEditor()->IsInGameMode())
{
// don't let certain keys fall through to the game when it's running
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(e);
auto key = keyEvent->key();
if ((key == Qt::Key_Alt) || (key == Qt::Key_AltGr) || ((key >= Qt::Key_F1) && (key <= Qt::Key_F35)))
{
return true;
}
}
}
break;
case QEvent::Shortcut:
{
// Eat shortcuts in game mode or when a guarded widget is being dragged
if (GetIEditor()->IsInGameMode() || m_widgetDraggedState == WidgetDraggedState::Dragged)
{
return true;
}
}
break;
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
case QEvent::MouseMove:
{
#if AZ_TRAIT_OS_PLATFORM_APPLE
auto widget = qobject_cast<QWidget*>(obj);
if (widget && widget->graphicsProxyWidget() != nullptr)
{
QMouseEvent* me = static_cast<QMouseEvent*>(e);
QWidget* target = qApp->widgetAt(QCursor::pos());
if (target)
{
QMouseEvent ev(me->type(), target->mapFromGlobal(QCursor::pos()), me->button(), me->buttons(), me->modifiers());
qApp->notify(target, &ev);
return true;
}
}
#endif
GuardMouseEventSelectionChangeMetrics(e);
}
break;
}
return GlobalEventFilter::eventFilter(obj, e);
}
private:
bool m_mouseButtonWasDown = false;
void GuardMouseEventSelectionChangeMetrics(QEvent* e)
{
// Force the metrics collector to queue up any selection changed metrics until mouse release, so that we don't
// get flooded with multiple selection changed events when one, sent on mouse release, is enough.
if (e->type() == QEvent::MouseButtonPress)
{
if (!m_mouseButtonWasDown)
{
m_mouseButtonWasDown = true;
}
}
else if (e->type() == QEvent::MouseButtonRelease)
{
// This is a tricky case. We don't want to send the end selection change event too early
// because there might be other things responding to the mouse release after this, and we want to
// block handling of the selection change events until we're entirely finished with the mouse press.
// So, queue the handling with a single shot timer, but then check the state of the mouse buttons
// to ensure that they haven't been pressed in between the release and the timer firing off.
QTimer::singleShot(0, this, [this]() {
if (!QApplication::mouseButtons() && m_mouseButtonWasDown)
{
m_mouseButtonWasDown = false;
}
});
}
}
//! Detect if the event's target is a Widget we want to guard from shortcuts while it's being dragged.
//! This function can be easily expanded to handle exceptions.
bool IsDragGuardedWidget(const QObject* obj)
{
return qobject_cast<const QWidget*>(obj) != nullptr;
}
//! Enum to keep track of Widget dragged state
enum class WidgetDraggedState
{
None, //!< No widget is being clicked nor dragged
Clicked, //!< A widget has been clicked on but has not been dragged
Dragged, //!< A widget is being dragged
};
WidgetDraggedState m_widgetDraggedState = WidgetDraggedState::None;
};
static void LogToDebug([[maybe_unused]] QtMsgType Type, [[maybe_unused]] const QMessageLogContext& Context, const QString& message)
{
AZ::Debug::Platform::OutputToDebugger("Qt", message.toUtf8().data());
AZ::Debug::Platform::OutputToDebugger(nullptr, "\n");
}
}
namespace Editor
{
void ScanDirectories(QFileInfoList& directoryList, const QStringList& filters, QFileInfoList& files, ScanDirectoriesUpdateCallBack updateCallback)
{
while (!directoryList.isEmpty())
{
QDir directory(directoryList.front().absoluteFilePath(), "*", QDir::Name | QDir::IgnoreCase, QDir::AllEntries);
directoryList.pop_front();
if (directory.exists())
{
// Append each file from this directory that matches one of the filters to files
directory.setNameFilters(filters);
directory.setFilter(QDir::Files);
files.append(directory.entryInfoList());
// Add all of the subdirectories from this directory to the queue to be searched
directory.setNameFilters(QStringList("*"));
directory.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
directoryList.append(directory.entryInfoList());
if (updateCallback)
{
updateCallback();
}
}
}
}
EditorQtApplication::EditorQtApplication(int& argc, char** argv)
: AzQtApplication(argc, argv)
, m_inWinEventFilter(false)
, m_stylesheet(new AzQtComponents::O3DEStylesheet(this))
, m_idleTimer(new QTimer(this))
{
m_idleTimer->setInterval(UninitializedFrequency);
setWindowIcon(QIcon(":/Application/res/o3de_editor.ico"));
// set the default key store for our preferences:
setApplicationName("O3DE Editor");
connect(m_idleTimer, &QTimer::timeout, this, &EditorQtApplication::maybeProcessIdle);
connect(this, &QGuiApplication::applicationStateChanged, this, [this] { ResetIdleTimerInterval(PollState); });
installEventFilter(this);
// Disable our debugging input helpers by default
QLoggingCategory::setFilterRules(QStringLiteral("o3de.editor.input.*=false"));
// Initialize our stylesheet here to allow Gems to register stylesheets when their system components activate.
AZ::IO::FixedMaxPath engineRootPath;
{
// Create a ComponentApplication to initialize the AZ::SystemAllocator and initialize the SettingsRegistry
AZ::ComponentApplication application(argc, argv);
auto settingsRegistry = AZ::SettingsRegistry::Get();
settingsRegistry->Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
}
m_stylesheet->initialize(this, engineRootPath);
}
void EditorQtApplication::Initialize()
{
GetIEditor()->RegisterNotifyListener(this);
// install QTranslator
InstallEditorTranslators();
// install hooks and filters last and revoke them first
InstallFilters();
// install this filter. It will be a parent of the application and cleaned up when it is cleaned up automically
auto globalEventFilter = new EditorGlobalEventFilter(this);
installEventFilter(globalEventFilter);
}
void EditorQtApplication::LoadSettings()
{
AZ::SerializeContext* context;
EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(context, "No serialize context");
char resolvedPath[AZ_MAX_PATH_LEN];
AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/EditorUserSettings.xml", resolvedPath, AZ_MAX_PATH_LEN);
m_localUserSettings.Load(resolvedPath, context);
m_localUserSettings.Activate(AZ::UserSettings::CT_LOCAL);
AZ::UserSettingsOwnerRequestBus::Handler::BusConnect(AZ::UserSettings::CT_LOCAL);
m_activatedLocalUserSettings = true;
}
void EditorQtApplication::UnloadSettings()
{
if (m_activatedLocalUserSettings)
{
SaveSettings();
m_localUserSettings.Deactivate();
AZ::UserSettingsOwnerRequestBus::Handler::BusDisconnect();
m_activatedLocalUserSettings = false;
}
}
void EditorQtApplication::SaveSettings()
{
if (m_activatedLocalUserSettings)
{
AZ::SerializeContext* context;
EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(context, "No serialize context");
char resolvedPath[AZ_MAX_PATH_LEN];
AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/EditorUserSettings.xml", resolvedPath, AZ_ARRAY_SIZE(resolvedPath));
m_localUserSettings.Save(resolvedPath, context);
}
}
void EditorQtApplication::maybeProcessIdle()
{
if (!m_isMovingOrResizing)
{
if (auto winapp = CCryEditApp::instance())
{
winapp->OnIdle(0);
}
}
}
void EditorQtApplication::InstallQtLogHandler()
{
qInstallMessageHandler(LogToDebug);
}
void EditorQtApplication::InstallFilters()
{
if (auto dispatcher = QAbstractEventDispatcher::instance())
{
dispatcher->installNativeEventFilter(this);
}
}
void EditorQtApplication::UninstallFilters()
{
if (auto dispatcher = QAbstractEventDispatcher::instance())
{
dispatcher->removeNativeEventFilter(this);
}
}
EditorQtApplication::~EditorQtApplication()
{
if (GetIEditor())
{
GetIEditor()->UnregisterNotifyListener(this);
}
UninstallFilters();
UninstallEditorTranslators();
}
#if defined(AZ_PLATFORM_WINDOWS)
bool EditorQtApplication::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long* result)
{
MSG* msg = (MSG*)message;
if (msg->message == WM_MOVING || msg->message == WM_SIZING)
{
m_isMovingOrResizing = true;
}
else if (msg->message == WM_EXITSIZEMOVE)
{
m_isMovingOrResizing = false;
}
// Prevent the user from being able to move the window in game mode.
// This is done during the hit test phase to bypass the native window move messages. If the window
// decoration wrapper title bar contains the cursor, set the result to HTCLIENT instead of
// HTCAPTION.
if (msg->message == WM_NCHITTEST && GetIEditor()->IsInGameMode())
{
const LRESULT defWinProcResult = DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
if (defWinProcResult == 1)
{
if (QWidget* widget = QWidget::find((WId)msg->hwnd))
{
if (auto wrapper = qobject_cast<const AzQtComponents::WindowDecorationWrapper *>(widget))
{
AzQtComponents::TitleBar* titleBar = wrapper->titleBar();
const short global_x = static_cast<short>(LOWORD(msg->lParam));
const short global_y = static_cast<short>(HIWORD(msg->lParam));
const QPoint globalPos = QHighDpi::fromNativePixels(QPoint(global_x, global_y), widget->window()->windowHandle());
const QPoint local = titleBar->mapFromGlobal(globalPos);
if (titleBar->draggableRect().contains(local) && !titleBar->isTopResizeArea(globalPos))
{
*result = HTCLIENT;
return true;
}
}
}
}
}
// Ensure that the Windows WM_INPUT messages get passed through to the AzFramework input system.
// These events are now consumed both in and out of game mode.
if (msg->message == WM_INPUT)
{
UINT rawInputSize;
const UINT rawInputHeaderSize = sizeof(RAWINPUTHEADER);
GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, NULL, &rawInputSize, rawInputHeaderSize);
AZStd::array<BYTE, sizeof(RAWINPUT)> rawInputBytesArray;
LPBYTE rawInputBytes = rawInputBytesArray.data();
const UINT bytesCopied = GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, rawInputBytes, &rawInputSize, rawInputHeaderSize);
CRY_ASSERT(bytesCopied == rawInputSize);
RAWINPUT* rawInput = (RAWINPUT*)rawInputBytes;
CRY_ASSERT(rawInput);
AzFramework::RawInputNotificationBusWindows::Broadcast(&AzFramework::RawInputNotificationsWindows::OnRawInputEvent, *rawInput);
return false;
}
else if (msg->message == WM_DEVICECHANGE)
{
if (msg->wParam == 0x0007) // DBT_DEVNODES_CHANGED
{
AzFramework::RawInputNotificationBusWindows::Broadcast(&AzFramework::RawInputNotificationsWindows::OnRawInputDeviceChangeEvent);
}
return true;
}
return false;
}
#endif
void EditorQtApplication::OnEditorNotifyEvent(EEditorNotifyEvent event)
{
switch (event)
{
case eNotify_OnStyleChanged:
RefreshStyleSheet();
emit skinChanged();
break;
case eNotify_OnQuit:
GetIEditor()->UnregisterNotifyListener(this);
break;
case eNotify_OnBeginGameMode:
// GetIEditor()->IsInGameMode() Isn't reliable when called from within the notification handler
ResetIdleTimerInterval(GameMode);
break;
case eNotify_OnEndGameMode:
ResetIdleTimerInterval(EditorMode);
break;
}
}
QColor EditorQtApplication::InterpolateColors(QColor a, QColor b, float factor)
{
return QColor(int(a.red() * (1.0f - factor) + b.red() * factor),
int(a.green() * (1.0f - factor) + b.green() * factor),
int(a.blue() * (1.0f - factor) + b.blue() * factor),
int(a.alpha() * (1.0f - factor) + b.alpha() * factor));
}
void EditorQtApplication::RefreshStyleSheet()
{
m_stylesheet->Refresh();
}
void EditorQtApplication::setIsMovingOrResizing(bool isMovingOrResizing)
{
if (m_isMovingOrResizing == isMovingOrResizing)
{
return;
}
m_isMovingOrResizing = isMovingOrResizing;
}
bool EditorQtApplication::isMovingOrResizing() const
{
return m_isMovingOrResizing;
}
const QColor& EditorQtApplication::GetColorByName(const QString& name)
{
return m_stylesheet->GetColorByName(name);
}
EditorQtApplication* EditorQtApplication::instance()
{
return static_cast<EditorQtApplication*>(QApplication::instance());
}
bool EditorQtApplication::IsActive()
{
return applicationState() == Qt::ApplicationActive;
}
QTranslator* EditorQtApplication::CreateAndInitializeTranslator(const QString& filename, const QString& directory)
{
Q_ASSERT(QFile::exists(directory + "/" + filename));
QTranslator* translator = new QTranslator();
translator->load(filename, directory);
installTranslator(translator);
return translator;
}
void EditorQtApplication::InstallEditorTranslators()
{
m_editorTranslator = CreateAndInitializeTranslator("editor_en-us.qm", ":/Translations");
m_assetBrowserTranslator = CreateAndInitializeTranslator("assetbrowser_en-us.qm", ":/Translations");
}
void EditorQtApplication::DeleteTranslator(QTranslator*& translator)
{
removeTranslator(translator);
delete translator;
translator = nullptr;
}
void EditorQtApplication::UninstallEditorTranslators()
{
DeleteTranslator(m_editorTranslator);
DeleteTranslator(m_assetBrowserTranslator);
}
void EditorQtApplication::EnableOnIdle(bool enable)
{
if (enable)
{
if (m_idleTimer->interval() == UninitializedFrequency)
{
ResetIdleTimerInterval();
}
m_idleTimer->start();
}
else
{
m_idleTimer->stop();
}
}
bool EditorQtApplication::OnIdleEnabled() const
{
if (m_idleTimer->interval() == UninitializedFrequency)
{
return false;
}
return m_idleTimer->isActive();
}
void EditorQtApplication::ResetIdleTimerInterval(TimerResetFlag flag)
{
bool isInGameMode = flag == GameMode;
if (flag == PollState)
{
isInGameMode = GetIEditor() ? GetIEditor()->IsInGameMode() : false;
}
// Game mode takes precedence over anything else
if (isInGameMode)
{
m_idleTimer->setInterval(GameModeIdleFrequency);
}
else
{
if (applicationState() & Qt::ApplicationActive)
{
m_idleTimer->setInterval(EditorModeIdleFrequency);
}
else
{
m_idleTimer->setInterval(InactiveModeFrequency);
}
}
}
bool EditorQtApplication::eventFilter(QObject* object, QEvent* event)
{
switch (event->type())
{
case QEvent::MouseButtonPress:
m_pressedButtons |= reinterpret_cast<QMouseEvent*>(event)->button();
break;
case QEvent::MouseButtonRelease:
m_pressedButtons &= ~(reinterpret_cast<QMouseEvent*>(event)->button());
break;
case QEvent::KeyPress:
m_pressedKeys.insert(reinterpret_cast<QKeyEvent*>(event)->key());
break;
case QEvent::KeyRelease:
m_pressedKeys.remove(reinterpret_cast<QKeyEvent*>(event)->key());
break;
#ifdef AZ_PLATFORM_WINDOWS
case QEvent::Leave:
{
// if we receive a leave event for a toolbar on Windows
// check first whether we really left it. If we didn't: start checking
// for the tool bar under the mouse by timer to check when we really left.
// Synthesize a new leave event then. Workaround for LY-69788
auto toolBarAt = [](const QPoint& pos) -> QToolBar* {
QWidget* widget = qApp->widgetAt(pos);
while (widget != nullptr)
{
if (QToolBar* tb = qobject_cast<QToolBar*>(widget))
{
return tb;
}
widget = widget->parentWidget();
}
return nullptr;
};
if (object == toolBarAt(QCursor::pos()))
{
QTimer* t = new QTimer(object);
t->start(100);
connect(t, &QTimer::timeout, object, [t, object, toolBarAt]() {
if (object != toolBarAt(QCursor::pos()))
{
QEvent event(QEvent::Leave);
qApp->sendEvent(object, &event);
t->deleteLater();
}
});
return true;
}
break;
}
#endif
default:
break;
}
return QApplication::eventFilter(object, event);
}
} // end namespace Editor
#include <Core/moc_QtEditorApplication.cpp>