QtForPythonSystemComponent.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <QtForPythonSystemComponent.h>
  9. #include <AzCore/Serialization/SerializeContext.h>
  10. #include <AzCore/RTTI/BehaviorContext.h>
  11. #include <AzFramework/StringFunc/StringFunc.h>
  12. #include <AzToolsFramework/API/EditorWindowRequestBus.h>
  13. #include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
  14. #include <AzCore/IO/SystemFile.h>
  15. #include <AzFramework/IO/LocalFileIO.h>
  16. #include <EditorPythonBindings/EditorPythonBindingsSymbols.h>
  17. AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // (qwidget.h) 'uint': forcing value to bool 'true' or 'false' (performance warning)
  18. #include <QPointer>
  19. #include <QWidget>
  20. #include <QApplication>
  21. #include <QDateTime>
  22. AZ_POP_DISABLE_WARNING
  23. // Qt defines slots, which interferes with the use here.
  24. #pragma push_macro("slots")
  25. #undef slots
  26. #include <Python.h>
  27. #include <pybind11/pybind11.h>
  28. #include <pybind11/functional.h>
  29. #pragma pop_macro("slots")
  30. namespace QtForPython
  31. {
  32. static const constexpr int s_loopTimerInterval = 5;
  33. static const constexpr float s_maxTime = 25.f * 60.f * 60.f;
  34. class QtForPythonEventHandler : public QObject
  35. {
  36. private:
  37. std::function<void()> m_loopCallback;
  38. float m_time = 0.f;
  39. QPointer<QObject> m_lastTimerParent = nullptr;
  40. int m_lastTimerId = 0;
  41. public:
  42. void SetupTimer(QObject* parent)
  43. {
  44. if (parent == m_lastTimerParent)
  45. {
  46. return;
  47. }
  48. if (m_lastTimerParent)
  49. {
  50. m_lastTimerParent->killTimer(m_lastTimerId);
  51. }
  52. m_lastTimerId = parent->startTimer(s_loopTimerInterval, Qt::CoarseTimer);
  53. m_lastTimerParent = parent;
  54. }
  55. QtForPythonEventHandler(QObject* parent = nullptr)
  56. : QObject(parent)
  57. {
  58. qApp->installEventFilter(this);
  59. SetupTimer(this);
  60. }
  61. float GetTime() const
  62. {
  63. return m_time;
  64. }
  65. void RunEventLoop()
  66. {
  67. if (m_loopCallback)
  68. {
  69. try
  70. {
  71. m_loopCallback();
  72. }
  73. catch (pybind11::error_already_set& pythonError)
  74. {
  75. // Release the exception stack and let Python print it
  76. pythonError.restore();
  77. PyErr_Print();
  78. }
  79. }
  80. }
  81. bool eventFilter(QObject* obj, QEvent* event)
  82. {
  83. // Determine which object should own our event loop timer
  84. // By default it's this object
  85. QObject* activeTimerParent = this;
  86. // If it's a modal or popup widget, use that to ensure we get timer events
  87. if (qApp->activePopupWidget())
  88. {
  89. activeTimerParent = qApp->activePopupWidget();
  90. }
  91. else if (qApp->activeModalWidget())
  92. {
  93. activeTimerParent = qApp->activeModalWidget();
  94. }
  95. SetupTimer(activeTimerParent);
  96. if (obj == m_lastTimerParent && event->type() == QEvent::Timer && static_cast<QTimerEvent*>(event)->timerId() == m_lastTimerId)
  97. {
  98. m_time += s_loopTimerInterval / 1000.f;
  99. if (m_time > s_maxTime)
  100. {
  101. m_time = 0.f;
  102. }
  103. RunEventLoop();
  104. }
  105. return false;
  106. }
  107. void SetLoopCallback(std::function<void()> callback)
  108. {
  109. m_loopCallback = callback;
  110. }
  111. void ClearLoopCallback()
  112. {
  113. m_loopCallback = {};
  114. }
  115. bool HasLoopCallback() const
  116. {
  117. return m_loopCallback.operator bool();
  118. }
  119. };
  120. void QtForPythonSystemComponent::Reflect(AZ::ReflectContext* context)
  121. {
  122. if (auto* serialize = azrtti_cast<AZ::SerializeContext*>(context))
  123. {
  124. serialize->Class<QtForPythonSystemComponent, AZ::Component>()
  125. ->Version(0)
  126. ;
  127. serialize->RegisterGenericType<QWidget>();
  128. }
  129. if (AZ::BehaviorContext* behavior = azrtti_cast<AZ::BehaviorContext*>(context))
  130. {
  131. behavior->EBus<QtForPythonRequestBus>("QtForPythonRequestBus")
  132. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  133. ->Attribute(AZ::Script::Attributes::Module, "qt")
  134. ->Event("IsActive", &QtForPythonRequestBus::Events::IsActive)
  135. ->Event("GetQtBootstrapParameters", &QtForPythonRequestBus::Events::GetQtBootstrapParameters)
  136. ;
  137. behavior->Class<QtBootstrapParameters>()
  138. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  139. ->Attribute(AZ::Script::Attributes::Module, "qt")
  140. ->Property("qtBinaryFolder", BehaviorValueProperty(&QtBootstrapParameters::m_qtBinaryFolder))
  141. ->Property("qtPluginsFolder", BehaviorValueProperty(&QtBootstrapParameters::m_qtPluginsFolder))
  142. ->Property("mainWindowId", BehaviorValueProperty(&QtBootstrapParameters::m_mainWindowId))
  143. ;
  144. }
  145. }
  146. void QtForPythonSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
  147. {
  148. provided.push_back(AZ_CRC_CE("QtForPythonService"));
  149. }
  150. void QtForPythonSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
  151. {
  152. incompatible.push_back(AZ_CRC_CE("QtForPythonService"));
  153. }
  154. void QtForPythonSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
  155. {
  156. required.push_back(EditorPythonBindings::PythonEmbeddedService);
  157. }
  158. void QtForPythonSystemComponent::Activate()
  159. {
  160. m_eventHandler = new QtForPythonEventHandler;
  161. QtForPythonRequestBus::Handler::BusConnect();
  162. EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusConnect();
  163. }
  164. void QtForPythonSystemComponent::Deactivate()
  165. {
  166. QtForPythonRequestBus::Handler::BusDisconnect();
  167. EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusDisconnect();
  168. delete m_eventHandler;
  169. }
  170. bool QtForPythonSystemComponent::IsActive() const
  171. {
  172. return AzToolsFramework::EditorPythonRunnerRequestBus::HasHandlers();
  173. }
  174. QtBootstrapParameters QtForPythonSystemComponent::GetQtBootstrapParameters() const
  175. {
  176. QtBootstrapParameters params;
  177. params.m_mainWindowId = 0;
  178. using namespace AzToolsFramework;
  179. QWidget* activeWindow = nullptr;
  180. EditorWindowRequestBus::BroadcastResult(activeWindow, &EditorWindowRequests::GetAppMainWindow);
  181. if (activeWindow)
  182. {
  183. // store the Qt main window so that scripts can hook into the main menu and/or docking framework
  184. params.m_mainWindowId = aznumeric_cast<AZ::u64>(activeWindow->winId());
  185. }
  186. // prepare the folder where the build system placed the QT binary files
  187. AZ::ComponentApplicationBus::BroadcastResult(params.m_qtBinaryFolder, &AZ::ComponentApplicationBus::Events::GetExecutableFolder);
  188. // prepare the QT plugins folder
  189. AZ::StringFunc::Path::Join(params.m_qtBinaryFolder.c_str(), "EditorPlugins", params.m_qtPluginsFolder);
  190. return params;
  191. }
  192. void QtForPythonSystemComponent::OnImportModule(PyObject* module)
  193. {
  194. // Register azlmbr.qt_helpers for our event loop callback
  195. pybind11::module parentModule = pybind11::cast<pybind11::module>(module);
  196. std::string pythonModuleName = pybind11::cast<std::string>(parentModule.attr("__name__"));
  197. if (AzFramework::StringFunc::Equal(pythonModuleName.c_str(), "azlmbr"))
  198. {
  199. pybind11::module helperModule = parentModule.def_submodule("qt_helpers");
  200. helperModule.def("set_loop_callback", [this](std::function<void()> callback)
  201. {
  202. if (m_eventHandler)
  203. {
  204. m_eventHandler->SetLoopCallback(callback);
  205. }
  206. }, R"delim(
  207. Sets a callback that will be invoked periodically during the course of Qt's event loop (even if a nested event loop is running).
  208. This is intended for internal use in pyside_utils and should generally not be used directly.)delim");
  209. helperModule.def("clear_loop_callback", [this]()
  210. {
  211. if (m_eventHandler)
  212. {
  213. m_eventHandler->ClearLoopCallback();
  214. }
  215. }, R"delim(
  216. Clears callback that will be invoked periodically during the course of Qt's event loop.
  217. This is intended for internal use in pyside_utils and should generally not be used directly.)delim");
  218. helperModule.def("loop_is_running", [this]()
  219. {
  220. if (m_eventHandler)
  221. {
  222. return m_eventHandler->HasLoopCallback();
  223. }
  224. return false;
  225. }, R"delim(
  226. Returns True if the qt_helper event_loop callback is set and running.
  227. This is intended for internal use in pyside_utils and should generally not be used directly.)delim");
  228. helperModule.def("time", [this]()
  229. {
  230. if (m_eventHandler)
  231. {
  232. return m_eventHandler->GetTime();
  233. }
  234. return -1.f;
  235. }, R"delim(
  236. Returns a floating timestamp, measured in seconds, that updates with the Qt event loop.
  237. This is intended for internal use in pyside_utils and should generally not be used directly.)delim");
  238. }
  239. }
  240. }