Start browsing at project overview, instead of inside one project.

Also moved from using project_uuid and node_uuid to using CloudPath
objects.
This commit is contained in:
Sybren A. Stüvel 2016-05-18 11:56:46 +02:00
parent 07f28d3072
commit 64d36818fe
3 changed files with 87 additions and 56 deletions

View File

@ -30,14 +30,6 @@ class BlenderCloudPreferences(AddonPreferences):
get=lambda self: PILLAR_SERVER_URL get=lambda self: PILLAR_SERVER_URL
) )
# TODO: Move to the Scene properties?
project_uuid = bpy.props.StringProperty(
name='Project UUID',
description='UUID of the current Blender Cloud project',
default='5672beecc0261b2005ed1a33',
get=lambda self: '5672beecc0261b2005ed1a33'
)
local_texture_dir = StringProperty( local_texture_dir = StringProperty(
name='Default Blender Cloud texture storage directory', name='Default Blender Cloud texture storage directory',
subtype='DIR_PATH', subtype='DIR_PATH',
@ -154,14 +146,9 @@ def register():
addon_prefs = preferences() addon_prefs = preferences()
WindowManager.blender_cloud_project = StringProperty( WindowManager.last_blender_cloud_location = StringProperty(
name="Blender Cloud project UUID", name="Last Blender Cloud browser location",
default=addon_prefs.project_uuid) # TODO: don't hard-code this default="/")
WindowManager.blender_cloud_node = StringProperty(
name="Blender Cloud node UUID",
default='') # empty == top-level of project
def default_if_empty(scene, context): def default_if_empty(scene, context):
"""The scene's local_texture_dir, if empty, reverts to the addon prefs.""" """The scene's local_texture_dir, if empty, reverts to the addon prefs."""

View File

@ -44,13 +44,27 @@ library_path = '/tmp'
library_icons_path = os.path.join(os.path.dirname(__file__), "icons") library_icons_path = os.path.join(os.path.dirname(__file__), "icons")
class UpNode(pillarsdk.Node): class SpecialFolderNode(pillarsdk.Node):
pass
class UpNode(SpecialFolderNode):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self['_id'] = 'UP' self['_id'] = 'UP'
self['node_type'] = 'UP' self['node_type'] = 'UP'
class ProjectNode(SpecialFolderNode):
def __init__(self, project):
super().__init__()
assert isinstance(project, pillarsdk.Project), 'wrong type for project: %r' % type(project)
self.merge(project.to_dict())
self['node_type'] = 'PROJECT'
class MenuItem: class MenuItem:
"""GUI menu item for the 3D View GUI.""" """GUI menu item for the 3D View GUI."""
@ -66,7 +80,7 @@ class MenuItem:
'SPINNER': os.path.join(library_icons_path, 'spinner.png'), 'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
} }
SUPPORTED_NODE_TYPES = {'UP', 'group_texture', 'texture'} SUPPORTED_NODE_TYPES = {'UP', 'PROJECT', 'group_texture', 'texture'}
def __init__(self, node, file_desc, thumb_path: str, label_text): def __init__(self, node, file_desc, thumb_path: str, label_text):
self.log = logging.getLogger('%s.MenuItem' % __name__) self.log = logging.getLogger('%s.MenuItem' % __name__)
@ -75,12 +89,15 @@ class MenuItem:
raise TypeError('Node of type %r not supported; supported are %r.' % ( raise TypeError('Node of type %r not supported; supported are %r.' % (
node['node_type'], self.SUPPORTED_NODE_TYPES)) node['node_type'], self.SUPPORTED_NODE_TYPES))
assert isinstance(node, pillarsdk.Node), 'wrong type for node: %r' % type(node)
assert isinstance(node['_id'], str), 'wrong type for node["_id"]: %r' % type(node['_id'])
self.node = node # pillarsdk.Node, contains 'node_type' key to indicate type self.node = node # pillarsdk.Node, contains 'node_type' key to indicate type
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node. self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
self.label_text = label_text self.label_text = label_text
self._thumb_path = '' self._thumb_path = ''
self.icon = None self.icon = None
self._is_folder = node['node_type'] == 'group_texture' or isinstance(node, UpNode) self._is_folder = node['node_type'] == 'group_texture' or \
isinstance(node, SpecialFolderNode)
self.thumb_path = thumb_path self.thumb_path = thumb_path
@ -186,12 +203,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
_state = 'INITIALIZING' _state = 'INITIALIZING'
project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID current_path = pillar.CloudPath('/')
node = None # The Node object we're currently showing, or None if we're at the project top.
node_uuid = '' # Blender Cloud node UUID we're currently showing, i.e. None-safe self.node['_id']
# This contains a stack of Node objects that lead up to the currently browsed node. # This contains a stack of Node objects that lead up to the currently browsed node.
# This allows us to display the "up" item.
path_stack = [] path_stack = []
async_task = None # asyncio task for fetching thumbnails async_task = None # asyncio task for fetching thumbnails
@ -200,7 +214,6 @@ class BlenderCloudBrowser(bpy.types.Operator):
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__) log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
_menu_item_lock = threading.Lock() _menu_item_lock = threading.Lock()
current_path = ''
current_display_content = [] current_display_content = []
loaded_images = set() loaded_images = set()
thumbnails_cache = '' thumbnails_cache = ''
@ -217,9 +230,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
return {'CANCELLED'} return {'CANCELLED'}
wm = context.window_manager wm = context.window_manager
self.project_uuid = wm.blender_cloud_project
self.node_uuid = wm.blender_cloud_node self.current_path = pillar.CloudPath(wm.last_blender_cloud_location)
self.path_stack = [] self.path_stack = [] # list of nodes that make up the current path.
self.thumbnails_cache = cache.cache_directory('thumbnails') self.thumbnails_cache = cache.cache_directory('thumbnails')
self.mouse_x = event.mouse_x self.mouse_x = event.mouse_x
@ -355,21 +368,28 @@ class BlenderCloudBrowser(bpy.types.Operator):
Also keeps track of the current node, so that we know where the "up" button should go. Also keeps track of the current node, so that we know where the "up" button should go.
""" """
assert isinstance(node, pillarsdk.Node), 'Wrong type %s' % node
# Going up or down? # Going up or down?
if self.path_stack and isinstance(node, UpNode): if isinstance(node, UpNode):
self.log.debug('Going up, pop the stack; pre-pop stack is %r', self.path_stack) self.log.debug('Going up to %r', self.current_path)
node = self.path_stack.pop() self.current_path = self.current_path.parent
if self.path_stack:
self.path_stack.pop()
else: else:
# Going down, keep track of where we were (project top-level is None) # Going down, keep track of where we were
self.path_stack.append(self.node) self.current_path /= node['_id']
self.log.debug('Going up, push the stack; post-push stack is %r', self.path_stack) self.log.debug('Going down to %r', self.current_path)
self.path_stack.append(node)
# Set 'current' to the given node
self.node_uuid = node['_id'] if node else None
self.node = node
self.browse_assets() self.browse_assets()
@property
def node(self):
if not self.path_stack:
return None
return self.path_stack[-1]
def _stop_async_task(self): def _stop_async_task(self):
self.log.debug('Stopping async task') self.log.debug('Stopping async task')
if self.async_task is None: if self.async_task is None:
@ -456,6 +476,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
thumbnails_directory = self.thumbnails_cache thumbnails_directory = self.thumbnails_cache
self.log.info('Asynchronously downloading previews to %r', thumbnails_directory) self.log.info('Asynchronously downloading previews to %r', thumbnails_directory)
self.log.info('Current BCloud path is %r', self.current_path)
self.clear_images() self.clear_images()
def thumbnail_loading(node, texture_node): def thumbnail_loading(node, texture_node):
@ -464,27 +485,33 @@ class BlenderCloudBrowser(bpy.types.Operator):
def thumbnail_loaded(node, file_desc, thumb_path): def thumbnail_loaded(node, file_desc, thumb_path):
self.update_menu_item(node, file_desc, thumb_path, file_desc['filename']) self.update_menu_item(node, file_desc, thumb_path, file_desc['filename'])
project_uuid = self.current_path.project_uuid
node_uuid = self.current_path.node_uuid
# Download either by group_texture node UUID or by project UUID (which # Download either by group_texture node UUID or by project UUID (which
# shows all top-level nodes) # shows all top-level nodes)
if self.node_uuid: if node_uuid:
self.log.debug('Getting subnodes for parent node %r', self.node_uuid) self.log.debug('Getting subnodes for parent node %r', node_uuid)
children = await pillar.get_nodes(parent_node_uuid=self.node_uuid, children = await pillar.get_nodes(parent_node_uuid=node_uuid,
node_type='group_textures') node_type='group_textures')
# Make sure we can go up again. # Make sure we can go up again.
if self.path_stack: if self.path_stack:
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..') self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
elif self.project_uuid: elif project_uuid:
self.log.debug('Getting subnodes for project node %r', self.project_uuid) self.log.debug('Getting subnodes for project node %r', project_uuid)
children = await pillar.get_nodes(self.project_uuid, '') children = await pillar.get_nodes(project_uuid, '')
else: else:
# TODO: add "nothing here" icon and trigger re-draw # Query for projects
self.log.warning("Not node UUID and no project UUID, I can't do anything!") self.log.warning("Not node UUID and no project UUID, I can't do anything!")
children = await pillar.get_texture_projects()
for proj_dict in children:
self.add_menu_item(ProjectNode(proj_dict), None, 'FOLDER', proj_dict['name'])
return return
# Download all child nodes # Download all child nodes
self.log.debug('Iterating over child nodes of %r', self.node_uuid) self.log.debug('Iterating over child nodes of %r', node_uuid)
for child in children: for child in children:
# print(' - %(_id)s = %(name)s' % child) # print(' - %(_id)s = %(name)s' % child)
if child['node_type'] not in MenuItem.SUPPORTED_NODE_TYPES: if child['node_type'] not in MenuItem.SUPPORTED_NODE_TYPES:
@ -494,20 +521,20 @@ class BlenderCloudBrowser(bpy.types.Operator):
# There are only sub-nodes at the project level, no texture nodes, # There are only sub-nodes at the project level, no texture nodes,
# so we won't have to bother looking for textures. # so we won't have to bother looking for textures.
if not self.node_uuid: if not node_uuid:
return return
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid) directory = os.path.join(thumbnails_directory, project_uuid, node_uuid)
os.makedirs(directory, exist_ok=True) os.makedirs(directory, exist_ok=True)
self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid) self.log.debug('Fetching texture thumbnails for node %r', node_uuid)
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory, await pillar.fetch_texture_thumbs(node_uuid, 's', directory,
thumbnail_loading=thumbnail_loading, thumbnail_loading=thumbnail_loading,
thumbnail_loaded=thumbnail_loaded, thumbnail_loaded=thumbnail_loaded,
future=self.signalling_future) future=self.signalling_future)
def browse_assets(self): def browse_assets(self):
self.log.debug('Browsing assets at project %r node %r', self.project_uuid, self.node_uuid) self.log.debug('Browsing assets at %r', self.current_path)
self._new_async_task(self.async_download_previews()) self._new_async_task(self.async_download_previews())
def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future = None): def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future = None):
@ -694,8 +721,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
self.clear_images() self.clear_images()
self._state = 'DOWNLOADING_TEXTURE' self._state = 'DOWNLOADING_TEXTURE'
project_uuid = self.current_path.project_uuid
node_path_components = [node['name'] for node in self.path_stack if node is not None] node_path_components = [node['name'] for node in self.path_stack if node is not None]
local_path_components = [self.project_uuid] + node_path_components + [self.node['name']] local_path_components = [project_uuid] + node_path_components
top_texture_directory = bpy.path.abspath(context.scene.local_texture_dir) top_texture_directory = bpy.path.abspath(context.scene.local_texture_dir)
local_path = os.path.join(top_texture_directory, *local_path_components) local_path = os.path.join(top_texture_directory, *local_path_components)

View File

@ -63,6 +63,8 @@ class CloudPath(pathlib.PurePosixPath):
@property @property
def project_uuid(self) -> str: def project_uuid(self) -> str:
assert self.parts[0] == '/' assert self.parts[0] == '/'
if len(self.parts) <= 1:
return None
return self.parts[1] return self.parts[1]
@property @property
@ -72,11 +74,10 @@ class CloudPath(pathlib.PurePosixPath):
@property @property
def node_uuid(self) -> str: def node_uuid(self) -> str:
node_uuids = self.node_uuids if len(self.parts) <= 2:
if not node_uuids:
return None return None
return node_uuids[-1]
return self.parts[-1]
@contextmanager @contextmanager
@ -270,6 +271,21 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
return children['_items'] return children['_items']
async def get_texture_projects() -> list:
"""Returns project dicts that contain textures."""
try:
children = await pillar_call(pillarsdk.Project.all, {
'where': {'node_types.name': 'texture'},
'sort': 'name',
})
except pillarsdk.ResourceNotFound as ex:
log.warning('Unable to find texture projects: %s', ex)
raise PillarError('Unable to find texture projects: %s' % ex)
return children['_items']
async def download_to_file(url, filename, *, async def download_to_file(url, filename, *,
header_store: str, header_store: str,
chunk_size=100 * 1024, chunk_size=100 * 1024,
@ -381,7 +397,7 @@ async def fetch_thumbnail_info(file: pillarsdk.File, directory: str, desired_siz
finished. finished.
""" """
thumb_link = await pillar_call(file.thumbnail_file, desired_size) thumb_link = await pillar_call(file.thumbnail, desired_size)
if thumb_link is None: if thumb_link is None:
raise ValueError("File {} has no thumbnail of size {}" raise ValueError("File {} has no thumbnail of size {}"