From 5c2beaf7b25101cf339ac1f6d0d3e86291457b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Mon, 21 Mar 2016 11:46:47 +0100 Subject: [PATCH] Browsing now uses node objects, instead of just node UUID This made the browsing code slightly simpler, and reduces the number of Pillar queries. The list of nodes visited to end up at the current node is also stored, so that we can easily go back up. --- blender_cloud/gui.py | 125 ++++++++++++++++++++++++++++------------ blender_cloud/pillar.py | 18 +++--- 2 files changed, 94 insertions(+), 49 deletions(-) diff --git a/blender_cloud/gui.py b/blender_cloud/gui.py index 6c56c3a..576050b 100644 --- a/blender_cloud/gui.py +++ b/blender_cloud/gui.py @@ -21,7 +21,6 @@ import asyncio import logging import threading -import traceback import bpy import bgl @@ -33,6 +32,7 @@ from bpy.props import (BoolProperty, EnumProperty, FloatProperty, FloatVectorProperty, IntProperty, StringProperty) +import pillarsdk from . import async_loop, pillar icon_width = 128 @@ -44,6 +44,13 @@ library_path = '/tmp' library_icons_path = os.path.join(os.path.dirname(__file__), "icons") +class UpNode(pillarsdk.Node): + def __init__(self): + super().__init__() + self['_id'] = 'UP' + self['node_type'] = 'UP' + + class MenuItem: """GUI menu item for the 3D View GUI.""" @@ -59,17 +66,19 @@ class MenuItem: 'SPINNER': os.path.join(library_icons_path, 'spinner.png'), } - def __init__(self, node_uuid: str, file_desc, thumb_path: str, label_text): - # TODO: change node_uuid to the actual pillarsdk.Node object. - # That way we can simply inspect node.node_type to distinguish between - # folders ('group_texture' type) and files ('texture' type). + SUPPORTED_NODE_TYPES = {'UP', 'group_texture', 'texture'} - self.node_uuid = node_uuid # pillarsdk.Node UUID of a texture node + def __init__(self, node, file_desc, thumb_path: str, label_text): + if node['node_type'] not in self.SUPPORTED_NODE_TYPES: + raise TypeError('Node of type %r not supported; supported are %r.' % ( + node.group_texture, self.SUPPORTED_NODE_TYPES)) + + 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.label_text = label_text self._thumb_path = '' self.icon = None - self._is_folder = file_desc is None and thumb_path == 'FOLDER' + self._is_folder = node['node_type'] == 'group_texture' or isinstance(node, UpNode) self.thumb_path = thumb_path @@ -91,7 +100,15 @@ class MenuItem: else: self.icon = None - def update(self, file_desc, thumb_path: str, label_text): + @property + def node_uuid(self) -> str: + return self.node['_id'] + + def update(self, node, file_desc, thumb_path: str, label_text): + # We can get updated information about our Node, but a MenuItem should + # always represent one node, and it shouldn't be shared between nodes. + assert self.node_uuid == node['_id'], "Don't change the node ID this MenuItem reflects, just create a new one." + self.node = node self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node. self.thumb_path = thumb_path self.label_text = label_text @@ -166,7 +183,13 @@ class BlenderCloudBrowser(bpy.types.Operator): _state = 'BROWSING' project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID - node_uuid = '' # Blender Cloud node UUID + 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 allows us to display the "up" item. + 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 @@ -187,6 +210,7 @@ class BlenderCloudBrowser(bpy.types.Operator): self.thumbnails_cache = wm.thumbnails_cache self.project_uuid = wm.blender_cloud_project self.node_uuid = wm.blender_cloud_node + self.path_stack = [] self.mouse_x = event.mouse_x self.mouse_y = event.mouse_y @@ -237,8 +261,7 @@ class BlenderCloudBrowser(bpy.types.Operator): return {'FINISHED'} if selected.is_folder: - self.node_uuid = selected.node_uuid - self.browse_assets() + self.descend_node(selected.node) else: if selected.file_desc is None: # This can happen when the thumbnail information isn't loaded yet. @@ -253,6 +276,27 @@ class BlenderCloudBrowser(bpy.types.Operator): return {'RUNNING_MODAL'} + def descend_node(self, node): + """Descends the node hierarchy by visiting this node. + + Also keeps track of the current node, so that we know where the "up" button should go. + """ + + # Going up or down? + if self.path_stack and isinstance(node, UpNode): + self.log.debug('Going up, pop the stack; pre-pop stack is %r', self.path_stack) + node = self.path_stack.pop() + + else: + # Going down, keep track of where we were (project top-level is None) + self.path_stack.append(self.node) + self.log.debug('Going up, push the stack; post-push stack is %r', self.path_stack) + + # Set 'current' to the given node + self.node_uuid = node['_id'] if node else None + self.node = node + self.browse_assets() + def _stop_async_task(self): self.log.debug('Stopping async task') if self.async_task is None: @@ -317,12 +361,14 @@ class BlenderCloudBrowser(bpy.types.Operator): return menu_item - def update_menu_item(self, node_uuid, *args) -> MenuItem: + def update_menu_item(self, node, *args) -> MenuItem: + node_uuid = node['_id'] + # Just make this thread-safe to be on the safe side. with self._menu_item_lock: for menu_item in self.current_display_content: if menu_item.node_uuid == node_uuid: - menu_item.update(*args) + menu_item.update(node, *args) self.loaded_images.add(menu_item.icon.filepath_raw) break else: @@ -332,46 +378,49 @@ class BlenderCloudBrowser(bpy.types.Operator): self.log.info('Asynchronously downloading previews to %r', thumbnails_directory) self.clear_images() - def thumbnail_loading(node_uuid, texture_node): - self.add_menu_item(node_uuid, None, 'SPINNER', texture_node['name']) + def thumbnail_loading(node, texture_node): + self.add_menu_item(node, None, 'SPINNER', texture_node['name']) - def thumbnail_loaded(node_uuid, file_desc, thumb_path): - self.update_menu_item(node_uuid, file_desc, thumb_path, file_desc['filename']) + def thumbnail_loaded(node, file_desc, thumb_path): + self.update_menu_item(node, file_desc, thumb_path, file_desc['filename']) + # Download either by group_texture node UUID or by project UUID (which shows all top-level nodes) if self.node_uuid: self.log.debug('Getting subnodes for parent node %r', self.node_uuid) children = await pillar.get_nodes(parent_node_uuid=self.node_uuid, node_type='group_textures') - self.log.debug('Finding parent of node %r', self.node_uuid) # Make sure we can go up again. - parent_uuid = await pillar.parent_node_uuid(self.node_uuid) - self.add_menu_item(parent_uuid, None, 'FOLDER', '.. up ..') - - self.log.debug('Iterating over child nodes of %r', self.node_uuid) - for child in children: - # print(' - %(_id)s = %(name)s' % child) - self.add_menu_item(child['_id'], None, 'FOLDER', child['name']) - - directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid) - os.makedirs(directory, exist_ok=True) - - self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid) - await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory, - thumbnail_loading=thumbnail_loading, - thumbnail_loaded=thumbnail_loaded, - future=self.signalling_future) + if self.path_stack: + self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..') elif self.project_uuid: self.log.debug('Getting subnodes for project node %r', self.project_uuid) children = await pillar.get_nodes(self.project_uuid, '') - self.log.debug('Iterating over child nodes of project %r', self.project_uuid) - for child in children: - # print(' - %(_id)s = %(name)s' % child) - self.add_menu_item(child['_id'], None, 'FOLDER', child['name']) else: # TODO: add "nothing here" icon and trigger re-draw self.log.warning("Not node UUID and no project UUID, I can't do anything!") + return + + # Download all child nodes + self.log.debug('Iterating over child nodes of %r', self.node_uuid) + for child in children: + # print(' - %(_id)s = %(name)s' % child) + self.add_menu_item(child, None, 'FOLDER', child['name']) + + # There are only sub-nodes at the project level, no texture nodes, + # so we won't have to bother looking for textures. + if not self.node_uuid: + return + + directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid) + os.makedirs(directory, exist_ok=True) + + self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid) + await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory, + thumbnail_loading=thumbnail_loading, + thumbnail_loaded=thumbnail_loaded, + future=self.signalling_future) def browse_assets(self): self._state = 'BROWSING' diff --git a/blender_cloud/pillar.py b/blender_cloud/pillar.py index 35a9a13..fbf2fac 100644 --- a/blender_cloud/pillar.py +++ b/blender_cloud/pillar.py @@ -12,7 +12,6 @@ import pillarsdk.utils from . import http_cache - _pillar_api = None # will become a pillarsdk.Api object. log = logging.getLogger(__name__) uncached_session = requests.session() @@ -43,7 +42,7 @@ def blender_id_profile() -> dict: return blender_id.profiles.get_active_profile() -def pillar_api(pillar_endpoint: str=None) -> pillarsdk.Api: +def pillar_api(pillar_endpoint: str = None) -> pillarsdk.Api: """Returns the Pillar SDK API object for the current user. The user must be logged in. @@ -127,7 +126,8 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None, 'properties.order': 1, 'properties.status': 1, 'properties.content_type': 1, 'picture': 1}, 'where': where, - 'sort': 'properties.order'}, api=pillar_api()) + 'sort': 'properties.order', + 'embed': ['parent']}, api=pillar_api()) loop = asyncio.get_event_loop() children = await loop.run_in_executor(None, node_all) @@ -226,10 +226,10 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str, @param parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded. @param desired_size: size indicator, from 'sbtmlh'. @param thumbnail_directory: directory in which to store the downloaded thumbnails. - @param thumbnail_loading: callback function that takes (node_id, pillarsdk.File object) + @param thumbnail_loading: callback function that takes (pillarsdk.Node, pillarsdk.File) parameters, which is called before a thumbnail will be downloaded. This allows you to show a "downloading" indicator. - @param thumbnail_loaded: callback function that takes (node_id, pillarsdk.File object, + @param thumbnail_loaded: callback function that takes (pillarsdk.Node, pillarsdk.File object, thumbnail path) parameters, which is called for every thumbnail after it's been downloaded. @param future: Future that's inspected; if it is not None and cancelled, texture downloading is aborted. @@ -254,9 +254,7 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str, # Find the File that belongs to this texture node pic_uuid = texture_node['picture'] - loop.call_soon_threadsafe(functools.partial(thumbnail_loading, - texture_node['_id'], - texture_node)) + loop.call_soon_threadsafe(thumbnail_loading, texture_node, texture_node) file_desc = await loop.run_in_executor(None, file_find, pic_uuid) if file_desc is None: @@ -279,9 +277,7 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str, await download_to_file(thumb_url, thumb_path, future=future) - loop.call_soon_threadsafe(functools.partial(thumbnail_loaded, - texture_node['_id'], - file_desc, thumb_path)) + loop.call_soon_threadsafe(thumbnail_loaded, texture_node, file_desc, thumb_path) # Download all texture nodes in parallel. log.debug('Getting child nodes of node %r', parent_node_uuid)