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/Gems/QtForPython/Code/Source/QtForPythonSystemComponent.cpp

284 lines
10 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 <QtForPythonSystemComponent.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/API/EditorWindowRequestBus.h>
#include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
#include <AzCore/IO/SystemFile.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <EditorPythonBindings/EditorPythonBindingsSymbols.h>
AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // (qwidget.h) 'uint': forcing value to bool 'true' or 'false' (performance warning)
#include <QPointer>
#include <QWidget>
#include <QApplication>
#include <QDateTime>
AZ_POP_DISABLE_WARNING
// Qt defines slots, which interferes with the use here.
#pragma push_macro("slots")
#undef slots
#include <Python.h>
#include <pybind11/pybind11.h>
#include <pybind11/functional.h>
#pragma pop_macro("slots")
namespace QtForPython
{
static const constexpr int s_loopTimerInterval = 5;
static const constexpr float s_maxTime = 25.f * 60.f * 60.f;
class QtForPythonEventHandler : public QObject
{
private:
std::function<void()> m_loopCallback;
float m_time = 0.f;
QPointer<QObject> m_lastTimerParent = nullptr;
int m_lastTimerId = 0;
public:
void SetupTimer(QObject* parent)
{
if (parent == m_lastTimerParent)
{
return;
}
if (m_lastTimerParent)
{
m_lastTimerParent->killTimer(m_lastTimerId);
}
m_lastTimerId = parent->startTimer(s_loopTimerInterval, Qt::CoarseTimer);
m_lastTimerParent = parent;
}
QtForPythonEventHandler(QObject* parent = nullptr)
: QObject(parent)
{
qApp->installEventFilter(this);
SetupTimer(this);
}
float GetTime() const
{
return m_time;
}
void RunEventLoop()
{
if (m_loopCallback)
{
try
{
m_loopCallback();
}
catch (pybind11::error_already_set& pythonError)
{
// Release the exception stack and let Python print it
pythonError.restore();
PyErr_Print();
}
}
}
bool eventFilter(QObject* obj, QEvent* event)
{
// Determine which object should own our event loop timer
// By default it's this object
QObject* activeTimerParent = this;
// If it's a modal or popup widget, use that to ensure we get timer events
if (qApp->activePopupWidget())
{
activeTimerParent = qApp->activePopupWidget();
}
else if (qApp->activeModalWidget())
{
activeTimerParent = qApp->activeModalWidget();
}
SetupTimer(activeTimerParent);
if (obj == m_lastTimerParent && event->type() == QEvent::Timer && static_cast<QTimerEvent*>(event)->timerId() == m_lastTimerId)
{
m_time += s_loopTimerInterval / 1000.f;
if (m_time > s_maxTime)
{
m_time = 0.f;
}
RunEventLoop();
}
return false;
}
void SetLoopCallback(std::function<void()> callback)
{
m_loopCallback = callback;
}
void ClearLoopCallback()
{
m_loopCallback = {};
}
bool HasLoopCallback() const
{
return m_loopCallback.operator bool();
}
};
void QtForPythonSystemComponent::Reflect(AZ::ReflectContext* context)
{
if (auto* serialize = azrtti_cast<AZ::SerializeContext*>(context))
{
serialize->Class<QtForPythonSystemComponent, AZ::Component>()
->Version(0)
;
serialize->RegisterGenericType<QWidget>();
}
if (AZ::BehaviorContext* behavior = azrtti_cast<AZ::BehaviorContext*>(context))
{
behavior->EBus<QtForPythonRequestBus>("QtForPythonRequestBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "qt")
->Event("IsActive", &QtForPythonRequestBus::Events::IsActive)
->Event("GetQtBootstrapParameters", &QtForPythonRequestBus::Events::GetQtBootstrapParameters)
;
behavior->Class<QtBootstrapParameters>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "qt")
->Property("qtBinaryFolder", BehaviorValueProperty(&QtBootstrapParameters::m_qtBinaryFolder))
->Property("qtPluginsFolder", BehaviorValueProperty(&QtBootstrapParameters::m_qtPluginsFolder))
->Property("mainWindowId", BehaviorValueProperty(&QtBootstrapParameters::m_mainWindowId))
;
}
}
void QtForPythonSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC_CE("QtForPythonService"));
}
void QtForPythonSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC_CE("QtForPythonService"));
}
void QtForPythonSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
{
required.push_back(EditorPythonBindings::PythonEmbeddedService);
}
void QtForPythonSystemComponent::Activate()
{
m_eventHandler = new QtForPythonEventHandler;
QtForPythonRequestBus::Handler::BusConnect();
EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusConnect();
}
void QtForPythonSystemComponent::Deactivate()
{
QtForPythonRequestBus::Handler::BusDisconnect();
EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusDisconnect();
delete m_eventHandler;
}
bool QtForPythonSystemComponent::IsActive() const
{
return AzToolsFramework::EditorPythonRunnerRequestBus::HasHandlers();
}
QtBootstrapParameters QtForPythonSystemComponent::GetQtBootstrapParameters() const
{
QtBootstrapParameters params;
char devroot[AZ_MAX_PATH_LEN];
AZ::IO::FileIOBase::GetInstance()->ResolvePath("@devroot@", devroot, AZ_MAX_PATH_LEN);
#if !defined(Q_OS_WIN)
#error Unsupported OS platform for this QtForPython gem
#endif
params.m_mainWindowId = 0;
using namespace AzToolsFramework;
QWidget* activeWindow = nullptr;
EditorWindowRequestBus::BroadcastResult(activeWindow, &EditorWindowRequests::GetAppMainWindow);
if (activeWindow)
{
// store the Qt main window so that scripts can hook into the main menu and/or docking framework
params.m_mainWindowId = aznumeric_cast<AZ::u64>(activeWindow->winId());
}
// prepare the folder where the build system placed the QT binary files
AZ::ComponentApplicationBus::BroadcastResult(params.m_qtBinaryFolder, &AZ::ComponentApplicationBus::Events::GetExecutableFolder);
// prepare the QT plugins folder
AZ::StringFunc::Path::Join(params.m_qtBinaryFolder.c_str(), "EditorPlugins", params.m_qtPluginsFolder);
return params;
}
void QtForPythonSystemComponent::OnImportModule(PyObject* module)
{
// Register azlmbr.qt_helpers for our event loop callback
pybind11::module parentModule = pybind11::cast<pybind11::module>(module);
std::string pythonModuleName = pybind11::cast<std::string>(parentModule.attr("__name__"));
if (AzFramework::StringFunc::Equal(pythonModuleName.c_str(), "azlmbr"))
{
pybind11::module helperModule = parentModule.def_submodule("qt_helpers");
helperModule.def("set_loop_callback", [this](std::function<void()> callback)
{
if (m_eventHandler)
{
m_eventHandler->SetLoopCallback(callback);
}
}, R"delim(
Sets a callback that will be invoked periodically during the course of Qt's event loop (even if a nested event loop is running).
This is intended for internal use in pyside_utils and should generally not be used directly.)delim");
helperModule.def("clear_loop_callback", [this]()
{
if (m_eventHandler)
{
m_eventHandler->ClearLoopCallback();
}
}, R"delim(
Clears callback that will be invoked periodically during the course of Qt's event loop.
This is intended for internal use in pyside_utils and should generally not be used directly.)delim");
helperModule.def("loop_is_running", [this]()
{
if (m_eventHandler)
{
return m_eventHandler->HasLoopCallback();
}
return false;
}, R"delim(
Returns True if the qt_helper event_loop callback is set and running.
This is intended for internal use in pyside_utils and should generally not be used directly.)delim");
helperModule.def("time", [this]()
{
if (m_eventHandler)
{
return m_eventHandler->GetTime();
}
return -1.f;
}, R"delim(
Returns a floating timestamp, measured in seconds, that updates with the Qt event loop.
This is intended for internal use in pyside_utils and should generally not be used directly.)delim");
}
}
}