diff --git a/blender_cloud/async_loop.py b/blender_cloud/async_loop.py index 503e1e2..6feed3b 100644 --- a/blender_cloud/async_loop.py +++ b/blender_cloud/async_loop.py @@ -130,6 +130,84 @@ class AsyncLoopModalOperator(bpy.types.Operator): return {'RUNNING_MODAL'} +# noinspection PyAttributeOutsideInit +class AsyncModalOperatorMixin: + async_task = None # asyncio task for fetching thumbnails + signalling_future = None # asyncio future for signalling that we want to cancel everything. + log = logging.getLogger('%s.AsyncModalOperatorMixin' % __name__) + + _state = 'INITIALIZING' + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + self.timer = context.window_manager.event_timer_add(1 / 15, context.window) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + task = self.async_task + + if self._state != 'EXCEPTION' and task and task.done() and not task.cancelled(): + ex = task.exception() + if ex is not None: + self._state = 'EXCEPTION' + self.log.error('Exception while running task: %s', ex) + return {'RUNNING_MODAL'} + + if self._state == 'QUIT': + self._finish(context) + return {'FINISHED'} + + return {'PASS_THROUGH'} + + def _finish(self, context): + self._stop_async_task() + context.window_manager.event_timer_remove(self.timer) + + def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future = None): + """Stops the currently running async task, and starts another one.""" + + self.log.debug('Setting up a new task %r, so any existing task must be stopped', async_task) + self._stop_async_task() + + # Download the previews asynchronously. + self.signalling_future = future or asyncio.Future() + self.async_task = asyncio.ensure_future(async_task) + self.log.debug('Created new task %r', self.async_task) + + # Start the async manager so everything happens. + ensure_async_loop() + + def _stop_async_task(self): + self.log.debug('Stopping async task') + if self.async_task is None: + self.log.debug('No async task, trivially stopped') + return + + # Signal that we want to stop. + self.async_task.cancel() + if not self.signalling_future.done(): + self.log.info("Signalling that we want to cancel anything that's running.") + self.signalling_future.cancel() + + # Wait until the asynchronous task is done. + if not self.async_task.done(): + self.log.info("blocking until async task is done.") + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(self.async_task) + except asyncio.CancelledError: + self.log.info('Asynchronous task was cancelled') + return + + # noinspection PyBroadException + try: + self.async_task.result() # This re-raises any exception of the task. + except asyncio.CancelledError: + self.log.info('Asynchronous task was cancelled') + except Exception: + self.log.exception("Exception from asynchronous task") + + def register(): bpy.utils.register_class(AsyncLoopModalOperator) diff --git a/blender_cloud/gui.py b/blender_cloud/gui.py index 05310c9..081e533 100644 --- a/blender_cloud/gui.py +++ b/blender_cloud/gui.py @@ -27,11 +27,6 @@ import bgl import blf import os -from bpy.types import AddonPreferences -from bpy.props import (BoolProperty, EnumProperty, - FloatProperty, FloatVectorProperty, - IntProperty, StringProperty) - import pillarsdk from . import async_loop, pillar, cache @@ -205,22 +200,18 @@ class MenuItem: return self.x < mouse_x < self.x + self.width and self.y < mouse_y < self.y + self.height -class BlenderCloudBrowser(bpy.types.Operator): +class BlenderCloudBrowser(async_loop.AsyncModalOperatorMixin, bpy.types.Operator): bl_idname = 'pillar.browser' bl_label = 'Blender Cloud Texture Browser' _draw_handle = None - _state = 'INITIALIZING' - current_path = pillar.CloudPath('/') project_name = '' # This contains a stack of Node objects that lead up to the currently browsed node. path_stack = [] - async_task = None # asyncio task for fetching thumbnails - signalling_future = None # asyncio future for signalling that we want to cancel everything. timer = None log = logging.getLogger('%s.BlenderCloudBrowser' % __name__) @@ -264,23 +255,14 @@ class BlenderCloudBrowser(bpy.types.Operator): self.check_credentials() context.window.cursor_modal_set('DEFAULT') - context.window_manager.modal_handler_add(self) - self.timer = context.window_manager.event_timer_add(1 / 15, context.window) + async_loop.AsyncModalOperatorMixin.invoke(self, context, event) return {'RUNNING_MODAL'} def modal(self, context, event): - task = self.async_task - if self._state != 'EXCEPTION' and task.done() and not task.cancelled(): - ex = task.exception() - if ex is not None: - self._state = 'EXCEPTION' - self.log.error('Exception while running task: %s', ex) - return {'RUNNING_MODAL'} - - if self._state == 'QUIT': - self._finish(context) - return {'FINISHED'} + result = async_loop.AsyncModalOperatorMixin.modal(self, context, event) + if not {'PASS_THROUGH', 'RUNNING_MODAL'}.intersection(result): + return result if event.type == 'TAB' and event.value == 'RELEASE': self.log.info('Ensuring async loop is running') @@ -407,43 +389,12 @@ class BlenderCloudBrowser(bpy.types.Operator): return None return self.path_stack[-1] - def _stop_async_task(self): - self.log.debug('Stopping async task') - if self.async_task is None: - self.log.debug('No async task, trivially stopped') - return - - # Signal that we want to stop. - self.async_task.cancel() - if not self.signalling_future.done(): - self.log.info("Signalling that we want to cancel anything that's running.") - self.signalling_future.cancel() - - # Wait until the asynchronous task is done. - if not self.async_task.done(): - self.log.info("blocking until async task is done.") - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(self.async_task) - except asyncio.CancelledError: - self.log.info('Asynchronous task was cancelled') - return - - # noinspection PyBroadException - try: - self.async_task.result() # This re-raises any exception of the task. - except asyncio.CancelledError: - self.log.info('Asynchronous task was cancelled') - except Exception: - self.log.exception("Exception from asynchronous task") - def _finish(self, context): self.log.debug('Finishing the modal operator') - self._stop_async_task() + async_loop.AsyncModalOperatorMixin._finish(self, context) self.clear_images() context.space_data.draw_handler_remove(self._draw_handle, 'WINDOW') - context.window_manager.event_timer_remove(self.timer) context.window.cursor_modal_restore() if self.maximized_area: @@ -567,20 +518,6 @@ class BlenderCloudBrowser(bpy.types.Operator): self.log.debug('Browsing assets at %r', self.current_path) self._new_async_task(self.async_download_previews()) - def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future = None): - """Stops the currently running async task, and starts another one.""" - - self.log.debug('Setting up a new task %r, so any existing task must be stopped', async_task) - self._stop_async_task() - - # Download the previews asynchronously. - self.signalling_future = future or asyncio.Future() - self.async_task = asyncio.ensure_future(async_task) - self.log.debug('Created new task %r', self.async_task) - - # Start the async manager so everything happens. - async_loop.ensure_async_loop() - def draw_menu(self, context): """Draws the GUI with OpenGL."""