diff --git a/blender_cloud/__init__.py b/blender_cloud/__init__.py index c78fe16..bf2ae51 100644 --- a/blender_cloud/__init__.py +++ b/blender_cloud/__init__.py @@ -67,8 +67,9 @@ def register(): gui = reload_mod('gui') async_loop = reload_mod('async_loop') settings_sync = reload_mod('settings_sync') + reload_mod('blendfile') else: - from . import blender, gui, async_loop, settings_sync + from . import blender, gui, async_loop, settings_sync, blendfile async_loop.setup_asyncio_executor() async_loop.register() diff --git a/blender_cloud/blender.py b/blender_cloud/blender.py index 755e913..066c9cd 100644 --- a/blender_cloud/blender.py +++ b/blender_cloud/blender.py @@ -19,10 +19,17 @@ log = logging.getLogger(__name__) def redraw(self, context): - log.debug('SyncStatusProperties.status = %s', self.status) context.area.tag_redraw() +def blender_syncable_versions(self, context): + bss = context.window_manager.blender_sync_status + versions = bss.available_blender_versions + if not versions: + return [('', 'No settings stored in your home project.', '')] + return [(v, v, '') for v in versions] + + class SyncStatusProperties(PropertyGroup): status = EnumProperty( items=[ @@ -33,6 +40,12 @@ class SyncStatusProperties(PropertyGroup): name='status', description='Current status of Blender Sync.', update=redraw) + + version = EnumProperty( + items=blender_syncable_versions, + name='Version of Blender from which to pull', + description='Version of Blender from which to pull') + message = StringProperty(name='message', update=redraw) level = EnumProperty( items=[ @@ -47,7 +60,21 @@ class SyncStatusProperties(PropertyGroup): assert len(level) == 1, 'level should be a set of one string, not %r' % level self.level = level.pop() self.message = message - log.error('REPORT %s: %s / %s', self, self.level, self.message) + + # Message can also be empty, just to erase it from the GUI. + # No need to actually log those. + if message: + log.log(logging._nameToLevel[self.level], message) + + # List of syncable versions is stored in 'available_blender_versions' ID property, + # because I don't know how to store a variable list of strings in a proper RNA property. + @property + def available_blender_versions(self) -> list: + return self.get('available_blender_versions', []) + + @available_blender_versions.setter + def available_blender_versions(self, new_versions): + self['available_blender_versions'] = new_versions class BlenderCloudPreferences(AddonPreferences): @@ -101,31 +128,28 @@ class BlenderCloudPreferences(AddonPreferences): help_text = ('To logout or change profile, ' 'go to the Blender ID add-on preferences.') - sub = layout.column(align=True) - sub.label(text=text, icon=icon) + # Authentication stuff + auth_box = layout.box() + auth_box.label(text=text, icon=icon) help_lines = textwrap.wrap(help_text, 80) for line in help_lines: - sub.label(text=line) + auth_box.label(text=line) + auth_box.operator("pillar.credentials_update") - sub = layout.column() + # Texture browser stuff + texture_box = layout.box() + texture_box.enabled = icon != 'ERROR' + sub = texture_box.column() sub.label(text='Local directory for downloaded textures') sub.prop(self, "local_texture_dir", text='Default') sub.prop(context.scene, "local_texture_dir", text='Current scene') - # options for Pillar - sub = layout.column() - sub.enabled = icon != 'ERROR' - - # TODO: let users easily pick a project. For now, we just use the - # hard-coded server URL and UUID of the textures project. - # sub.prop(self, "pillar_server") - # sub.prop(self, "project_uuid") - sub.operator("pillar.credentials_update") - + # Blender Sync stuff bss = context.window_manager.blender_sync_status - col = layout.column() - row = col.row() + bsync_box = layout.box() + bsync_box.enabled = icon != 'ERROR' + row = bsync_box.row().split(percentage=0.33) row.label('Blender Sync') icon_for_level = { @@ -134,25 +158,39 @@ class BlenderCloudPreferences(AddonPreferences): 'ERROR': 'ERROR', } message_container = row.row() - message_container.label(bss.message or '-idle-', icon=icon_for_level[bss.level]) - # message_container.enabled = bool(bss.message) + message_container.label(bss.message, icon=icon_for_level[bss.level]) message_container.alert = True # bss.level in {'WARNING', 'ERROR'} - sub = col.column() + sub = bsync_box.column() sub.enabled = bss.status in {'NONE', 'IDLE'} - row = sub.row() - row.operator('pillar.sync', text='Refresh', icon='FILE_REFRESH').action = 'REFRESH' - row.operator('pillar.sync', text='To Cloud').action = 'PUSH' + buttons = sub.column() + row_buttons = buttons.row().split(percentage=0.5) + row_pull = row_buttons.row(align=True) + row_push = row_buttons.row() - if 'available_blender_versions' in bss: - for version in bss['available_blender_versions']: - props = sub.operator('pillar.sync', icon='FILE_REFRESH', - text='From Cloud %s' % version) + row_push.operator('pillar.sync', + text='Save %i.%i settings to Cloud' % bpy.app.version[:2], + icon='TRIA_UP').action = 'PUSH' + + versions = bss.available_blender_versions + version = bss.version + if bss.status in {'NONE', 'IDLE'}: + if not versions or not version: + row_pull.operator('pillar.sync', + text='Find version to load from Cloud', + icon='TRIA_DOWN').action = 'REFRESH' + else: + props = row_pull.operator('pillar.sync', + text='Load %s settings from Cloud' % version, + icon='TRIA_DOWN') props.action = 'PULL' props.blender_version = version - - # sub.prop(bss, 'level') + row_pull.operator('pillar.sync', + text='', + icon='DOTSDOWN').action = 'SELECT' + else: + row_pull.label('Cloud Sync is running.') class PillarCredentialsUpdate(Operator): diff --git a/blender_cloud/settings_sync.py b/blender_cloud/settings_sync.py index 9980f6d..47630d7 100644 --- a/blender_cloud/settings_sync.py +++ b/blender_cloud/settings_sync.py @@ -1,4 +1,3 @@ - """Synchronises settings & startup file with the Cloud. Caching is disabled on many PillarSDK calls, as synchronisation can happen rapidly between multiple machines. This means that information can be outdated @@ -66,6 +65,7 @@ def async_set_blender_sync_status(set_status: str): bss.status = 'IDLE' return wrapper + return decorator @@ -240,7 +240,6 @@ async def available_blender_versions(home_project_id: str, user_id: str) -> list if sync_group is None: bss.report({'ERROR'}, 'No synced Blender settings in your home project') - log.warning('No synced Blender settings in your home project') log.debug('-- unable to find sync group for home_project_id=%r and user_id=%r', home_project_id, user_id) return [] @@ -259,13 +258,12 @@ async def available_blender_versions(home_project_id: str, user_id: str) -> list if not sync_nodes or not sync_nodes._items: bss.report({'ERROR'}, 'No synced Blender settings in your home project') - log.warning('No synced Blender settings in your home project') return [] - versions = sync_nodes._items - log.info('Versions: %s', versions) + versions = [node.name for node in sync_nodes._items] + log.debug('Versions: %s', versions) - return [node.name for node in versions] + return versions # noinspection PyAttributeOutsideInit @@ -285,6 +283,7 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, ('PUSH', 'Push', 'Push settings to the Blender Cloud'), ('PULL', 'Pull', 'Pull settings from the Blender Cloud'), ('REFRESH', 'Refresh', 'Refresh available versions'), + ('SELECT', 'Select', 'Select version to sync'), ], name='action') @@ -297,10 +296,11 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, bss.report(level, message) def invoke(self, context, event): - self.log.info('at invoke: self = %r', self) + if self.action == 'SELECT': + # Synchronous action + return self.action_select(context) - self.log.info('Pulling from Blender %s', self.blender_version) - if not self.blender_version: + if self.action in {'PUSH', 'PULL'} and not self.blender_version: self.bss_report({'ERROR'}, 'No Blender version to sync for was given.') return {'CANCELLED'} @@ -310,11 +310,47 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, self._new_async_task(self.async_execute(context)) return {'RUNNING_MODAL'} + def action_select(self, context): + """Allows selection of the Blender version to use. + + This is a synchronous action, as it requires a dialog box. + """ + + self.log.info('Performing action SELECT') + + # Do a refresh before we can show the dropdown. + fut = asyncio.ensure_future(self.async_execute(context, action_override='REFRESH')) + loop = asyncio.get_event_loop() + loop.run_until_complete(fut) + + self._state = 'SELECTING' + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + bss = bpy.context.window_manager.blender_sync_status + self.layout.prop(bss, 'version') + + def execute(self, context): + if self.action != 'SELECT': + log.debug('Ignoring execute() for action %r', self.action) + return {'FINISHED'} + + log.debug('Performing execute() for action %r', self.action) + # Perform the sync when the user closes the dialog box. + bss = bpy.context.window_manager.blender_sync_status + bpy.ops.pillar.sync('INVOKE_DEFAULT', + action='PULL', + blender_version=bss.version) + + return {'FINISHED'} + @async_set_blender_sync_status('SYNCING') - async def async_execute(self, context): + async def async_execute(self, context, *, action_override=None): """Entry point of the asynchronous operator.""" - self.bss_report({'INFO'}, 'Synchronizing settings %s with Blender Cloud' % self.action) + action = action_override or self.action + self.bss_report({'INFO'}, 'Communicating with Blender Cloud') + self.log.info('Performing action %s', action) try: self.user_id = await self.check_credentials(context) @@ -335,9 +371,9 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, may_create=may_create) self.sync_group_id = gid self.sync_group_versioned_id = subgid - self.log.info('Found top-level group node ID: %s', self.sync_group_id) - self.log.info('Found group node ID for %s: %s', - self.blender_version, self.sync_group_versioned_id) + self.log.debug('Found top-level group node ID: %s', self.sync_group_id) + self.log.debug('Found group node ID for %s: %s', + self.blender_version, self.sync_group_versioned_id) except sdk_exceptions.ForbiddenAccess: self.log.exception('Unable to find Group ID') self.bss_report({'ERROR'}, 'Unable to find sync folder.') @@ -345,12 +381,12 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, return # Perform the requested action. - action = { + action_method = { 'PUSH': self.action_push, 'PULL': self.action_pull, 'REFRESH': self.action_refresh, - }[self.action] - await action(context) + }[action] + await action_method(context) except Exception as ex: self.log.exception('Unexpected exception caught.') self.bss_report({'ERROR'}, 'Unexpected error: %s' % ex) @@ -378,17 +414,12 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, self.user_id, future=self.signalling_future) + await self.action_refresh(context) self.bss_report({'INFO'}, 'Settings pushed to Blender Cloud.') async def action_pull(self, context): """Loads files from the Pillar server.""" - # Refuse to start if the file hasn't been saved. - if context.blend_data.is_dirty: - self.bss_report({'ERROR'}, 'Please save your Blend file before pulling' - ' settings from the Blender Cloud.') - return - # If the sync group node doesn't exist, offer a list of groups that do. if self.sync_group_id is None: self.bss_report({'ERROR'}, 'There are no synced Blender settings in your home project.') @@ -396,7 +427,7 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, if self.sync_group_versioned_id is None: self.bss_report({'ERROR'}, 'Therre are no synced Blender settings for version %s' % - self.blender_version) + self.blender_version) return self.bss_report({'INFO'}, 'Pulling settings from Blender Cloud') @@ -405,7 +436,6 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, await self.download_settings_file(fname, tempdir) self.bss_report({'WARNING'}, 'Settings pulled from Cloud, restart Blender to load them.') - self.log.info('at end: self = %r', self) async def action_refresh(self, context): self.bss_report({'INFO'}, 'Refreshing available Blender versions.') @@ -416,11 +446,10 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, versions = await available_blender_versions(self.home_project_id, self.user_id) bss = bpy.context.window_manager.blender_sync_status - bss['available_blender_versions'] = versions + bss.available_blender_versions = versions self.bss_report({'INFO'}, '') - async def download_settings_file(self, fname: str, temp_dir: str): config_dir = pathlib.Path(bpy.utils.user_resource('CONFIG')) meta_path = cache.cache_directory('home-project', 'blender-sync')