Nice UI and proper refreshing versions & loading settings.
This commit is contained in:
parent
671e9f31fa
commit
e73e9d3df7
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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,8 +371,8 @@ 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')
|
||||||
@ -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.')
|
||||||
@ -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')
|
||||||
|
Reference in New Issue
Block a user