Nice UI and proper refreshing versions & loading settings.

This commit is contained in:
Sybren A. Stüvel 2016-06-24 12:53:49 +02:00
parent 671e9f31fa
commit e73e9d3df7
3 changed files with 126 additions and 58 deletions

View File

@ -67,8 +67,9 @@ def register():
gui = reload_mod('gui') gui = reload_mod('gui')
async_loop = reload_mod('async_loop') async_loop = reload_mod('async_loop')
settings_sync = reload_mod('settings_sync') settings_sync = reload_mod('settings_sync')
reload_mod('blendfile')
else: 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.setup_asyncio_executor()
async_loop.register() async_loop.register()

View File

@ -19,10 +19,17 @@ log = logging.getLogger(__name__)
def redraw(self, context): def redraw(self, context):
log.debug('SyncStatusProperties.status = %s', self.status)
context.area.tag_redraw() 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): class SyncStatusProperties(PropertyGroup):
status = EnumProperty( status = EnumProperty(
items=[ items=[
@ -33,6 +40,12 @@ class SyncStatusProperties(PropertyGroup):
name='status', name='status',
description='Current status of Blender Sync.', description='Current status of Blender Sync.',
update=redraw) 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) message = StringProperty(name='message', update=redraw)
level = EnumProperty( level = EnumProperty(
items=[ items=[
@ -47,7 +60,21 @@ class SyncStatusProperties(PropertyGroup):
assert len(level) == 1, 'level should be a set of one string, not %r' % level assert len(level) == 1, 'level should be a set of one string, not %r' % level
self.level = level.pop() self.level = level.pop()
self.message = message 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): class BlenderCloudPreferences(AddonPreferences):
@ -101,31 +128,28 @@ class BlenderCloudPreferences(AddonPreferences):
help_text = ('To logout or change profile, ' help_text = ('To logout or change profile, '
'go to the Blender ID add-on preferences.') 'go to the Blender ID add-on preferences.')
sub = layout.column(align=True) # Authentication stuff
sub.label(text=text, icon=icon) auth_box = layout.box()
auth_box.label(text=text, icon=icon)
help_lines = textwrap.wrap(help_text, 80) help_lines = textwrap.wrap(help_text, 80)
for line in help_lines: 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.label(text='Local directory for downloaded textures')
sub.prop(self, "local_texture_dir", text='Default') sub.prop(self, "local_texture_dir", text='Default')
sub.prop(context.scene, "local_texture_dir", text='Current scene') sub.prop(context.scene, "local_texture_dir", text='Current scene')
# options for Pillar # Blender Sync stuff
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")
bss = context.window_manager.blender_sync_status bss = context.window_manager.blender_sync_status
col = layout.column() bsync_box = layout.box()
row = col.row() bsync_box.enabled = icon != 'ERROR'
row = bsync_box.row().split(percentage=0.33)
row.label('Blender Sync') row.label('Blender Sync')
icon_for_level = { icon_for_level = {
@ -134,25 +158,39 @@ class BlenderCloudPreferences(AddonPreferences):
'ERROR': 'ERROR', 'ERROR': 'ERROR',
} }
message_container = row.row() message_container = row.row()
message_container.label(bss.message or '-idle-', icon=icon_for_level[bss.level]) message_container.label(bss.message, icon=icon_for_level[bss.level])
# message_container.enabled = bool(bss.message)
message_container.alert = True # bss.level in {'WARNING', 'ERROR'} message_container.alert = True # bss.level in {'WARNING', 'ERROR'}
sub = col.column() sub = bsync_box.column()
sub.enabled = bss.status in {'NONE', 'IDLE'} sub.enabled = bss.status in {'NONE', 'IDLE'}
row = sub.row() buttons = sub.column()
row.operator('pillar.sync', text='Refresh', icon='FILE_REFRESH').action = 'REFRESH' row_buttons = buttons.row().split(percentage=0.5)
row.operator('pillar.sync', text='To Cloud').action = 'PUSH' row_pull = row_buttons.row(align=True)
row_push = row_buttons.row()
if 'available_blender_versions' in bss: row_push.operator('pillar.sync',
for version in bss['available_blender_versions']: text='Save %i.%i settings to Cloud' % bpy.app.version[:2],
props = sub.operator('pillar.sync', icon='FILE_REFRESH', icon='TRIA_UP').action = 'PUSH'
text='From Cloud %s' % version)
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.action = 'PULL'
props.blender_version = version props.blender_version = version
row_pull.operator('pillar.sync',
# sub.prop(bss, 'level') text='',
icon='DOTSDOWN').action = 'SELECT'
else:
row_pull.label('Cloud Sync is running.')
class PillarCredentialsUpdate(Operator): class PillarCredentialsUpdate(Operator):

View File

@ -1,4 +1,3 @@
"""Synchronises settings & startup file with the Cloud. """Synchronises settings & startup file with the Cloud.
Caching is disabled on many PillarSDK calls, as synchronisation can happen Caching is disabled on many PillarSDK calls, as synchronisation can happen
rapidly between multiple machines. This means that information can be outdated 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' bss.status = 'IDLE'
return wrapper return wrapper
return decorator return decorator
@ -240,7 +240,6 @@ async def available_blender_versions(home_project_id: str, user_id: str) -> list
if sync_group is None: if sync_group is None:
bss.report({'ERROR'}, 'No synced Blender settings in your home project') 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', log.debug('-- unable to find sync group for home_project_id=%r and user_id=%r',
home_project_id, user_id) home_project_id, user_id)
return [] 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: if not sync_nodes or not sync_nodes._items:
bss.report({'ERROR'}, 'No synced Blender settings in your home project') bss.report({'ERROR'}, 'No synced Blender settings in your home project')
log.warning('No synced Blender settings in your home project')
return [] return []
versions = sync_nodes._items versions = [node.name for node in sync_nodes._items]
log.info('Versions: %s', versions) log.debug('Versions: %s', versions)
return [node.name for node in versions] return versions
# noinspection PyAttributeOutsideInit # noinspection PyAttributeOutsideInit
@ -285,6 +283,7 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
('PUSH', 'Push', 'Push settings to the Blender Cloud'), ('PUSH', 'Push', 'Push settings to the Blender Cloud'),
('PULL', 'Pull', 'Pull settings from the Blender Cloud'), ('PULL', 'Pull', 'Pull settings from the Blender Cloud'),
('REFRESH', 'Refresh', 'Refresh available versions'), ('REFRESH', 'Refresh', 'Refresh available versions'),
('SELECT', 'Select', 'Select version to sync'),
], ],
name='action') name='action')
@ -297,10 +296,11 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
bss.report(level, message) bss.report(level, message)
def invoke(self, context, event): 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 self.action in {'PUSH', 'PULL'} and not self.blender_version:
if not self.blender_version:
self.bss_report({'ERROR'}, 'No Blender version to sync for was given.') self.bss_report({'ERROR'}, 'No Blender version to sync for was given.')
return {'CANCELLED'} return {'CANCELLED'}
@ -310,11 +310,47 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
self._new_async_task(self.async_execute(context)) self._new_async_task(self.async_execute(context))
return {'RUNNING_MODAL'} 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_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.""" """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: try:
self.user_id = await self.check_credentials(context) self.user_id = await self.check_credentials(context)
@ -335,9 +371,9 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
may_create=may_create) may_create=may_create)
self.sync_group_id = gid self.sync_group_id = gid
self.sync_group_versioned_id = subgid self.sync_group_versioned_id = subgid
self.log.info('Found top-level group node ID: %s', self.sync_group_id) self.log.debug('Found top-level group node ID: %s', self.sync_group_id)
self.log.info('Found group node ID for %s: %s', self.log.debug('Found group node ID for %s: %s',
self.blender_version, self.sync_group_versioned_id) self.blender_version, self.sync_group_versioned_id)
except sdk_exceptions.ForbiddenAccess: except sdk_exceptions.ForbiddenAccess:
self.log.exception('Unable to find Group ID') self.log.exception('Unable to find Group ID')
self.bss_report({'ERROR'}, 'Unable to find sync folder.') self.bss_report({'ERROR'}, 'Unable to find sync folder.')
@ -345,12 +381,12 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
return return
# Perform the requested action. # Perform the requested action.
action = { action_method = {
'PUSH': self.action_push, 'PUSH': self.action_push,
'PULL': self.action_pull, 'PULL': self.action_pull,
'REFRESH': self.action_refresh, 'REFRESH': self.action_refresh,
}[self.action] }[action]
await action(context) await action_method(context)
except Exception as ex: except Exception as ex:
self.log.exception('Unexpected exception caught.') self.log.exception('Unexpected exception caught.')
self.bss_report({'ERROR'}, 'Unexpected error: %s' % ex) self.bss_report({'ERROR'}, 'Unexpected error: %s' % ex)
@ -378,17 +414,12 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
self.user_id, self.user_id,
future=self.signalling_future) future=self.signalling_future)
await self.action_refresh(context)
self.bss_report({'INFO'}, 'Settings pushed to Blender Cloud.') self.bss_report({'INFO'}, 'Settings pushed to Blender Cloud.')
async def action_pull(self, context): async def action_pull(self, context):
"""Loads files from the Pillar server.""" """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 the sync group node doesn't exist, offer a list of groups that do.
if self.sync_group_id is None: if self.sync_group_id is None:
self.bss_report({'ERROR'}, 'There are no synced Blender settings in your home project.') 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: if self.sync_group_versioned_id is None:
self.bss_report({'ERROR'}, 'Therre are no synced Blender settings for version %s' % self.bss_report({'ERROR'}, 'Therre are no synced Blender settings for version %s' %
self.blender_version) self.blender_version)
return return
self.bss_report({'INFO'}, 'Pulling settings from Blender Cloud') 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) await self.download_settings_file(fname, tempdir)
self.bss_report({'WARNING'}, 'Settings pulled from Cloud, restart Blender to load them.') 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): async def action_refresh(self, context):
self.bss_report({'INFO'}, 'Refreshing available Blender versions.') 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) versions = await available_blender_versions(self.home_project_id, self.user_id)
bss = bpy.context.window_manager.blender_sync_status bss = bpy.context.window_manager.blender_sync_status
bss['available_blender_versions'] = versions bss.available_blender_versions = versions
self.bss_report({'INFO'}, '') self.bss_report({'INFO'}, '')
async def download_settings_file(self, fname: str, temp_dir: str): async def download_settings_file(self, fname: str, temp_dir: str):
config_dir = pathlib.Path(bpy.utils.user_resource('CONFIG')) config_dir = pathlib.Path(bpy.utils.user_resource('CONFIG'))
meta_path = cache.cache_directory('home-project', 'blender-sync') meta_path = cache.cache_directory('home-project', 'blender-sync')