From 1401a3503f5aebd36c5eeea26448cb1f5d06b785 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 6 Dec 2024 13:36:45 +0100 Subject: [PATCH] fix wx --- examples/cube_wx.py | 4 ++-- rendercanvas/_loop.py | 26 +++++++++++++++++--------- rendercanvas/asyncio.py | 3 +++ rendercanvas/qt.py | 4 +++- rendercanvas/wx.py | 33 ++++++++++++++++++++++----------- 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/examples/cube_wx.py b/examples/cube_wx.py index b07784f..bd08daf 100644 --- a/examples/cube_wx.py +++ b/examples/cube_wx.py @@ -5,7 +5,7 @@ Run a wgpu example on the wx backend. """ -from rendercanvas.wx import RenderCanvas, run +from rendercanvas.wx import RenderCanvas, loop from rendercanvas.utils.cube import setup_drawing_sync @@ -18,4 +18,4 @@ if __name__ == "__main__": - run() + loop.run() diff --git a/rendercanvas/_loop.py b/rendercanvas/_loop.py index 54b4f75..da47cf1 100644 --- a/rendercanvas/_loop.py +++ b/rendercanvas/_loop.py @@ -28,7 +28,8 @@ class BaseLoop: * off (0): the initial state, the subclass should probably not even import dependencies yet. * ready (1): the first canvas is created, ``_rc_init()`` is called to get the loop ready for running. * active (2): the loop is active, but not running via our entrypoints. - * running (3): the loop is running via ``_rc_run()`` or ``_rc_run_async()``. + * active (3): the loop is inter-active in e.g. an IDE. + * running (4): the loop is running via ``_rc_run()`` or ``_rc_run_async()``. Notes: @@ -44,17 +45,21 @@ class BaseLoop: def __init__(self): self.__tasks = set() self.__canvas_groups = set() - self.__state = 0 # 0: idle, 1: ready, 2: active, 3: running via our entrypoint self.__should_stop = 0 + self.__state = ( + 0 # 0: off, 1: ready, 2: detected-active, 3: inter-active, 4: running + ) def __repr__(self): - state = ["off", "ready", "active", "running"][self.__state] - return f"<{self.__class__.__module__}.{self.__class__.__name__} '{state}' at {hex(id(self))}>" + full_class_name = f"{self.__class__.__module__}.{self.__class__.__name__}" + state = self.__state + state_str = ["off", "ready", "active", "active", "running"][state] + return f"<{full_class_name} '{state_str}' ({state}) at {hex(id(self))}>" def _mark_as_interactive(self): """For subclasses to set active from ``_rc_init()``""" - if self.__state == 1: - self.__state = 2 + if self.__state in (1, 2): + self.__state = 3 def _register_canvas_group(self, canvas_group): # A CanvasGroup will call this every time that a new canvas is created for this loop. @@ -201,7 +206,10 @@ def run(self): # Yes we can pass elif self.__state == 2: - # No, already active (interactive mode) + # We look active, but have not been marked interactive + pass + elif self.__state == 3: + # No, already marked active (interactive mode) return else: # No, what are you doing?? @@ -245,7 +253,7 @@ def stop(self): """Close all windows and stop the currently running event-loop. If the loop is active but not running via our ``run()`` method, the loop - moves back to its "off" state, but the underlying loop is not stopped. + moves back to its off-state, but the underlying loop is not stopped. """ # Only take action when we're inside the run() method self.__should_stop += 1 @@ -254,7 +262,7 @@ def stop(self): self._stop() def _stop(self): - """Move to our off state.""" + """Move to the off-state.""" # If we used the async adapter, cancel any tasks while self.__tasks: task = self.__tasks.pop() diff --git a/rendercanvas/asyncio.py b/rendercanvas/asyncio.py index fbbe786..1b0774a 100644 --- a/rendercanvas/asyncio.py +++ b/rendercanvas/asyncio.py @@ -34,6 +34,9 @@ def _rc_init(self): def _rc_run(self): import asyncio + if self._interactive_loop is not None: + return + asyncio.run(self._rc_run_async()) async def _rc_run_async(self): diff --git a/rendercanvas/qt.py b/rendercanvas/qt.py index b4c729e..c31fa19 100644 --- a/rendercanvas/qt.py +++ b/rendercanvas/qt.py @@ -166,6 +166,8 @@ def _rc_init(self): app = QtWidgets.QApplication.instance() if app is None: self._app = QtWidgets.QApplication([]) + if already_had_app_on_import: + self._mark_as_interactive() def _rc_run(self): # Note: we could detect if asyncio is running (interactive session) and wheter @@ -178,7 +180,7 @@ def _rc_run(self): # or not. if already_had_app_on_import: - return # Likely in an interactive session or larger application that will start the Qt app. + return self._we_run_the_loop = True try: diff --git a/rendercanvas/wx.py b/rendercanvas/wx.py index 7840550..3be6e37 100644 --- a/rendercanvas/wx.py +++ b/rendercanvas/wx.py @@ -144,10 +144,10 @@ class WxLoop(BaseLoop): def _rc_init(self): if self._app is None: - app = wx.App.GetInstance() - if app is None: + self._app = wx.App.GetInstance() + if self._app is None: self._app = wx.App() - wx.App.SetInstance(app) + wx.App.SetInstance(self._app) def _rc_run(self): self._app.MainLoop() @@ -166,7 +166,15 @@ def _rc_add_task(self, async_func, name): return super()._rc_add_task(async_func, name) def _rc_call_later(self, delay, callback): - raise NotImplementedError() # todo: wx.CallSoon(callback, args) + wx.CallLater(int(delay * 1000), callback) + + def process_wx_events(self): + old_loop = wx.GUIEventLoop.GetActive() + event_loop = wx.GUIEventLoop() + wx.EventLoop.SetActive(event_loop) + while event_loop.Pending(): + event_loop.Dispatch() + wx.EventLoop.SetActive(old_loop) loop = WxLoop() @@ -257,13 +265,7 @@ def _rc_gui_poll(self): if isinstance(self._rc_canvas_group.get_loop(), WxLoop): pass # all is well else: - old_loop = wx.EventLoop.GetActive() - event_loop = wx.EventLoop() - wx.EventLoop.SetActive(event_loop) - while event_loop.Pending(): - event_loop.Dispatch() - self.app.ProcessIdle() - wx.EventLoop.SetActive(old_loop) + loop.process_wx_events() def _rc_get_present_methods(self): if self._surface_ids is None: @@ -510,6 +512,15 @@ def Destroy(self): # noqa: N802 - this is a wx method self._subwidget._is_closed = True super().Destroy() + # wx stops running its loop as soon as the last canvas closes. + # So when that happens, we manually run the loop for a short while + # so that we can clean up properly + if not self._subwidget._rc_canvas_group.get_canvases(): + etime = time.perf_counter() + 0.15 + while time.perf_counter() < etime: + time.sleep(0.01) + loop.process_wx_events() + # Make available under a name that is the same for all gui backends RenderWidget = WxRenderWidget