2016-03-14 17:23:56 +01:00
|
|
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
|
|
#
|
|
|
|
# Copyright (C) 2014 Blender Aid
|
|
|
|
# http://www.blendearaid.com
|
|
|
|
# blenderaid@gmail.com
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
import asyncio
|
2016-03-15 13:47:21 +01:00
|
|
|
import logging
|
2016-03-14 17:23:56 +01:00
|
|
|
import threading
|
2016-07-15 16:56:55 +02:00
|
|
|
import os
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
import bpy
|
|
|
|
import bgl
|
|
|
|
import blf
|
|
|
|
|
2016-03-21 11:46:47 +01:00
|
|
|
import pillarsdk
|
2016-03-22 15:20:22 +01:00
|
|
|
from . import async_loop, pillar, cache
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-06-24 15:22:12 +02:00
|
|
|
REQUIRED_ROLES_FOR_TEXTURE_BROWSER = {'subscriber', 'demo'}
|
2016-07-15 17:01:24 +02:00
|
|
|
MOUSE_SCROLL_PIXELS_PER_TICK = 50
|
2016-06-24 15:22:12 +02:00
|
|
|
|
2016-07-15 16:59:52 +02:00
|
|
|
ICON_WIDTH = 128
|
|
|
|
ICON_HEIGHT = 128
|
|
|
|
TARGET_ITEM_WIDTH = 400
|
|
|
|
TARGET_ITEM_HEIGHT = 128
|
|
|
|
ITEM_MARGIN_X = 5
|
|
|
|
ITEM_MARGIN_Y = 5
|
|
|
|
ITEM_PADDING_X = 5
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
library_path = '/tmp'
|
|
|
|
library_icons_path = os.path.join(os.path.dirname(__file__), "icons")
|
|
|
|
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
class SpecialFolderNode(pillarsdk.Node):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class UpNode(SpecialFolderNode):
|
2016-03-21 11:46:47 +01:00
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
self['_id'] = 'UP'
|
|
|
|
self['node_type'] = 'UP'
|
|
|
|
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
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'
|
|
|
|
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
class MenuItem:
|
|
|
|
"""GUI menu item for the 3D View GUI."""
|
|
|
|
|
|
|
|
icon_margin_x = 4
|
|
|
|
icon_margin_y = 4
|
|
|
|
text_margin_x = 6
|
|
|
|
|
|
|
|
text_height = 16
|
|
|
|
text_width = 72
|
|
|
|
|
|
|
|
DEFAULT_ICONS = {
|
|
|
|
'FOLDER': os.path.join(library_icons_path, 'folder.png'),
|
2016-03-14 18:12:29 +01:00
|
|
|
'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
|
2016-03-14 17:23:56 +01:00
|
|
|
}
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
SUPPORTED_NODE_TYPES = {'UP', 'PROJECT', 'group_texture', 'texture'}
|
2016-03-21 11:46:47 +01:00
|
|
|
|
|
|
|
def __init__(self, node, file_desc, thumb_path: str, label_text):
|
2016-05-17 17:30:57 +02:00
|
|
|
self.log = logging.getLogger('%s.MenuItem' % __name__)
|
2016-03-21 11:46:47 +01:00
|
|
|
if node['node_type'] not in self.SUPPORTED_NODE_TYPES:
|
2016-05-17 17:30:57 +02:00
|
|
|
self.log.info('Invalid node type in node: %s', node)
|
2016-03-21 11:46:47 +01:00
|
|
|
raise TypeError('Node of type %r not supported; supported are %r.' % (
|
2016-05-17 17:30:57 +02:00
|
|
|
node['node_type'], self.SUPPORTED_NODE_TYPES))
|
2016-03-15 14:07:25 +01:00
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
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'])
|
2016-03-21 11:46:47 +01:00
|
|
|
self.node = node # pillarsdk.Node, contains 'node_type' key to indicate type
|
2016-03-14 17:23:56 +01:00
|
|
|
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
|
|
|
|
self.label_text = label_text
|
2016-03-14 18:12:29 +01:00
|
|
|
self._thumb_path = ''
|
|
|
|
self.icon = None
|
2016-05-18 12:50:51 +02:00
|
|
|
self._is_folder = (node['node_type'] == 'group_texture' or
|
|
|
|
isinstance(node, SpecialFolderNode))
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-05-18 14:54:49 +02:00
|
|
|
# Determine sorting order.
|
|
|
|
# by default, sort all the way at the end and folders first.
|
|
|
|
self._order = 0 if self._is_folder else 10000
|
|
|
|
if node and node.properties and node.properties.order is not None:
|
|
|
|
self._order = node.properties.order
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
self.thumb_path = thumb_path
|
|
|
|
|
|
|
|
# Updated when drawing the image
|
|
|
|
self.x = 0
|
|
|
|
self.y = 0
|
|
|
|
self.width = 0
|
|
|
|
self.height = 0
|
|
|
|
|
2016-05-18 14:54:49 +02:00
|
|
|
def sort_key(self):
|
|
|
|
"""Key for sorting lists of MenuItems."""
|
|
|
|
return self._order, self.label_text
|
|
|
|
|
2016-03-14 18:12:29 +01:00
|
|
|
@property
|
|
|
|
def thumb_path(self) -> str:
|
|
|
|
return self._thumb_path
|
|
|
|
|
|
|
|
@thumb_path.setter
|
|
|
|
def thumb_path(self, new_thumb_path: str):
|
|
|
|
self._thumb_path = self.DEFAULT_ICONS.get(new_thumb_path, new_thumb_path)
|
|
|
|
if self._thumb_path:
|
|
|
|
self.icon = bpy.data.images.load(filepath=self._thumb_path)
|
|
|
|
else:
|
|
|
|
self.icon = None
|
|
|
|
|
2016-03-21 11:46:47 +01:00
|
|
|
@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.
|
2016-03-22 13:26:24 +01:00
|
|
|
if self.node_uuid != node['_id']:
|
|
|
|
raise ValueError("Don't change the node ID this MenuItem reflects, "
|
|
|
|
"just create a new one.")
|
2016-03-21 11:46:47 +01:00
|
|
|
self.node = node
|
2016-03-14 18:12:29 +01:00
|
|
|
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
|
|
|
|
self.thumb_path = thumb_path
|
|
|
|
self.label_text = label_text
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
@property
|
|
|
|
def is_folder(self) -> bool:
|
2016-03-15 14:05:54 +01:00
|
|
|
return self._is_folder
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
def update_placement(self, x, y, width, height):
|
|
|
|
"""Use OpenGL to draw this one menu item."""
|
|
|
|
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.width = width
|
|
|
|
self.height = height
|
|
|
|
|
|
|
|
def draw(self, highlighted: bool):
|
|
|
|
bgl.glEnable(bgl.GL_BLEND)
|
|
|
|
if highlighted:
|
|
|
|
bgl.glColor4f(0.555, 0.555, 0.555, 0.8)
|
|
|
|
else:
|
|
|
|
bgl.glColor4f(0.447, 0.447, 0.447, 0.8)
|
|
|
|
|
|
|
|
bgl.glRectf(self.x, self.y, self.x + self.width, self.y + self.height)
|
|
|
|
|
|
|
|
texture = self.icon
|
|
|
|
err = texture.gl_load(filter=bgl.GL_NEAREST, mag=bgl.GL_NEAREST)
|
|
|
|
assert not err, 'OpenGL error: %i' % err
|
|
|
|
|
|
|
|
bgl.glColor4f(0.0, 0.0, 1.0, 0.5)
|
|
|
|
# bgl.glLineWidth(1.5)
|
|
|
|
|
|
|
|
# ------ TEXTURE ---------#
|
|
|
|
bgl.glBindTexture(bgl.GL_TEXTURE_2D, texture.bindcode[0])
|
|
|
|
bgl.glEnable(bgl.GL_TEXTURE_2D)
|
|
|
|
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
|
|
|
|
|
|
|
|
bgl.glColor4f(1, 1, 1, 1)
|
|
|
|
bgl.glBegin(bgl.GL_QUADS)
|
|
|
|
bgl.glTexCoord2d(0, 0)
|
|
|
|
bgl.glVertex2d(self.x + self.icon_margin_x, self.y)
|
|
|
|
bgl.glTexCoord2d(0, 1)
|
2016-07-15 16:59:52 +02:00
|
|
|
bgl.glVertex2d(self.x + self.icon_margin_x, self.y + ICON_HEIGHT)
|
2016-03-14 17:23:56 +01:00
|
|
|
bgl.glTexCoord2d(1, 1)
|
2016-07-15 16:59:52 +02:00
|
|
|
bgl.glVertex2d(self.x + self.icon_margin_x + ICON_WIDTH, self.y + ICON_HEIGHT)
|
2016-03-14 17:23:56 +01:00
|
|
|
bgl.glTexCoord2d(1, 0)
|
2016-07-15 16:59:52 +02:00
|
|
|
bgl.glVertex2d(self.x + self.icon_margin_x + ICON_WIDTH, self.y)
|
2016-03-14 17:23:56 +01:00
|
|
|
bgl.glEnd()
|
|
|
|
bgl.glDisable(bgl.GL_TEXTURE_2D)
|
|
|
|
bgl.glDisable(bgl.GL_BLEND)
|
|
|
|
|
|
|
|
texture.gl_free()
|
|
|
|
|
|
|
|
# draw some text
|
|
|
|
font_id = 0
|
|
|
|
blf.position(font_id,
|
2016-07-15 16:59:52 +02:00
|
|
|
self.x + self.icon_margin_x + ICON_WIDTH + self.text_margin_x,
|
|
|
|
self.y + ICON_HEIGHT * 0.5 - 0.25 * self.text_height, 0)
|
2016-03-14 17:23:56 +01:00
|
|
|
blf.size(font_id, self.text_height, self.text_width)
|
|
|
|
blf.draw(font_id, self.label_text)
|
|
|
|
|
|
|
|
def hits(self, mouse_x: int, mouse_y: int) -> bool:
|
|
|
|
return self.x < mouse_x < self.x + self.width and self.y < mouse_y < self.y + self.height
|
|
|
|
|
|
|
|
|
2016-06-24 15:00:38 +02:00
|
|
|
class BlenderCloudBrowser(pillar.PillarOperatorMixin,
|
|
|
|
async_loop.AsyncModalOperatorMixin,
|
|
|
|
bpy.types.Operator):
|
2016-03-14 17:23:56 +01:00
|
|
|
bl_idname = 'pillar.browser'
|
|
|
|
bl_label = 'Blender Cloud Texture Browser'
|
|
|
|
|
|
|
|
_draw_handle = None
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
current_path = pillar.CloudPath('/')
|
2016-05-18 14:35:24 +02:00
|
|
|
project_name = ''
|
2016-03-21 11:46:47 +01:00
|
|
|
|
|
|
|
# This contains a stack of Node objects that lead up to the currently browsed node.
|
|
|
|
path_stack = []
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
timer = None
|
2016-03-15 13:47:21 +01:00
|
|
|
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-03-14 18:12:29 +01:00
|
|
|
_menu_item_lock = threading.Lock()
|
2016-03-14 17:23:56 +01:00
|
|
|
current_display_content = []
|
|
|
|
loaded_images = set()
|
|
|
|
thumbnails_cache = ''
|
2016-03-15 17:09:27 +01:00
|
|
|
maximized_area = False
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
mouse_x = 0
|
|
|
|
mouse_y = 0
|
2016-07-15 17:01:24 +02:00
|
|
|
scroll_offset = 0
|
|
|
|
scroll_offset_target = 0
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
def invoke(self, context, event):
|
2016-05-04 14:30:54 +02:00
|
|
|
# Refuse to start if the file hasn't been saved.
|
2016-06-17 13:11:34 +02:00
|
|
|
if context.blend_data.is_dirty:
|
2016-05-04 14:30:54 +02:00
|
|
|
self.report({'ERROR'}, 'Please save your Blend file before using '
|
|
|
|
'the Blender Cloud addon.')
|
|
|
|
return {'CANCELLED'}
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
wm = context.window_manager
|
2016-05-18 11:56:46 +02:00
|
|
|
|
|
|
|
self.current_path = pillar.CloudPath(wm.last_blender_cloud_location)
|
|
|
|
self.path_stack = [] # list of nodes that make up the current path.
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-03-22 15:20:22 +01:00
|
|
|
self.thumbnails_cache = cache.cache_directory('thumbnails')
|
2016-03-15 17:09:27 +01:00
|
|
|
self.mouse_x = event.mouse_x
|
|
|
|
self.mouse_y = event.mouse_y
|
|
|
|
|
|
|
|
# See if we have to maximize the current area
|
|
|
|
if not context.screen.show_fullscreen:
|
|
|
|
self.maximized_area = True
|
|
|
|
bpy.ops.screen.screen_full_area(use_hide_panels=True)
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
# Add the region OpenGL drawing callback
|
|
|
|
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
|
2016-03-15 17:09:27 +01:00
|
|
|
self._draw_handle = context.space_data.draw_handler_add(
|
2016-03-14 17:23:56 +01:00
|
|
|
self.draw_menu, (context,), 'WINDOW', 'POST_PIXEL')
|
|
|
|
|
|
|
|
self.current_display_content = []
|
|
|
|
self.loaded_images = set()
|
2016-07-15 17:01:24 +02:00
|
|
|
self._scroll_reset()
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-05-10 14:33:02 +02:00
|
|
|
context.window.cursor_modal_set('DEFAULT')
|
2016-06-16 16:33:05 +02:00
|
|
|
async_loop.AsyncModalOperatorMixin.invoke(self, context, event)
|
2016-06-24 15:00:38 +02:00
|
|
|
self._new_async_task(self.async_execute(context))
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
|
|
|
def modal(self, context, event):
|
2016-06-16 16:33:05 +02:00
|
|
|
result = async_loop.AsyncModalOperatorMixin.modal(self, context, event)
|
|
|
|
if not {'PASS_THROUGH', 'RUNNING_MODAL'}.intersection(result):
|
|
|
|
return result
|
2016-03-15 15:44:19 +01:00
|
|
|
|
|
|
|
if event.type == 'TAB' and event.value == 'RELEASE':
|
|
|
|
self.log.info('Ensuring async loop is running')
|
|
|
|
async_loop.ensure_async_loop()
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
if event.type == 'TIMER':
|
2016-07-15 17:01:24 +02:00
|
|
|
self._scroll_smooth()
|
2016-03-14 17:23:56 +01:00
|
|
|
context.area.tag_redraw()
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
|
|
|
if 'MOUSE' in event.type:
|
|
|
|
context.area.tag_redraw()
|
2016-03-15 17:09:27 +01:00
|
|
|
self.mouse_x = event.mouse_x
|
|
|
|
self.mouse_y = event.mouse_y
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-05-10 14:52:51 +02:00
|
|
|
left_mouse_release = event.type == 'LEFTMOUSE' and event.value == 'RELEASE'
|
|
|
|
if self._state == 'PLEASE_SUBSCRIBE' and left_mouse_release:
|
|
|
|
self.open_browser_subscribe()
|
|
|
|
self._finish(context)
|
|
|
|
return {'FINISHED'}
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-05-10 14:52:51 +02:00
|
|
|
if self._state == 'BROWSING':
|
|
|
|
selected = self.get_clicked()
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-05-10 14:52:51 +02:00
|
|
|
if selected:
|
|
|
|
context.window.cursor_set('HAND')
|
2016-03-14 17:23:56 +01:00
|
|
|
else:
|
2016-05-10 14:52:51 +02:00
|
|
|
context.window.cursor_set('DEFAULT')
|
|
|
|
|
2016-07-15 17:01:24 +02:00
|
|
|
# Scrolling
|
|
|
|
if event.type == 'WHEELUPMOUSE':
|
|
|
|
self._scroll_by(MOUSE_SCROLL_PIXELS_PER_TICK)
|
|
|
|
context.area.tag_redraw()
|
|
|
|
elif event.type == 'WHEELDOWNMOUSE':
|
|
|
|
self._scroll_by(-MOUSE_SCROLL_PIXELS_PER_TICK)
|
|
|
|
context.area.tag_redraw()
|
|
|
|
|
2016-05-10 14:52:51 +02:00
|
|
|
if left_mouse_release:
|
|
|
|
if selected is None:
|
|
|
|
# No item clicked, ignore it.
|
2016-03-15 15:44:19 +01:00
|
|
|
return {'RUNNING_MODAL'}
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-05-10 14:52:51 +02:00
|
|
|
if selected.is_folder:
|
|
|
|
self.descend_node(selected.node)
|
|
|
|
else:
|
|
|
|
if selected.file_desc is None:
|
|
|
|
# This can happen when the thumbnail information isn't loaded yet.
|
|
|
|
# Just ignore the click for now.
|
|
|
|
# TODO: think of a way to handle this properly.
|
2016-05-18 12:50:51 +02:00
|
|
|
self.log.debug('Selected item %r has no file_desc', selected)
|
2016-05-10 14:52:51 +02:00
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
self.handle_item_selection(context, selected)
|
|
|
|
|
|
|
|
if event.type in {'RIGHTMOUSE', 'ESC'}:
|
2016-03-14 17:23:56 +01:00
|
|
|
self._finish(context)
|
|
|
|
return {'CANCELLED'}
|
|
|
|
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
2016-06-24 15:00:38 +02:00
|
|
|
async def async_execute(self, context):
|
2016-05-04 14:30:47 +02:00
|
|
|
self._state = 'CHECKING_CREDENTIALS'
|
|
|
|
self.log.debug('Checking credentials')
|
|
|
|
|
|
|
|
try:
|
2016-06-24 15:22:12 +02:00
|
|
|
user_id = await self.check_credentials(context, REQUIRED_ROLES_FOR_TEXTURE_BROWSER)
|
2016-05-10 14:52:51 +02:00
|
|
|
except pillar.NotSubscribedToCloudError:
|
|
|
|
self.log.info('User not subscribed to Blender Cloud.')
|
|
|
|
self._show_subscribe_screen()
|
2016-06-24 15:00:38 +02:00
|
|
|
return None
|
2016-05-04 14:30:47 +02:00
|
|
|
|
2016-06-24 15:00:38 +02:00
|
|
|
if user_id is None:
|
|
|
|
raise pillar.UserNotLoggedInError()
|
2016-05-04 14:30:47 +02:00
|
|
|
|
2016-06-24 15:00:38 +02:00
|
|
|
await self.async_download_previews()
|
2016-05-04 14:30:47 +02:00
|
|
|
|
2016-05-10 14:52:51 +02:00
|
|
|
def _show_subscribe_screen(self):
|
|
|
|
"""Shows the "You need to subscribe" screen."""
|
|
|
|
|
|
|
|
self._state = 'PLEASE_SUBSCRIBE'
|
|
|
|
bpy.context.window.cursor_set('HAND')
|
|
|
|
|
2016-03-21 11:46:47 +01:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
assert isinstance(node, pillarsdk.Node), 'Wrong type %s' % node
|
2016-03-21 11:46:47 +01:00
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
if isinstance(node, UpNode):
|
2016-05-18 14:35:24 +02:00
|
|
|
# Going up.
|
2016-05-18 11:56:46 +02:00
|
|
|
self.log.debug('Going up to %r', self.current_path)
|
|
|
|
self.current_path = self.current_path.parent
|
|
|
|
if self.path_stack:
|
|
|
|
self.path_stack.pop()
|
2016-05-18 14:35:24 +02:00
|
|
|
if not self.path_stack:
|
|
|
|
self.project_name = ''
|
2016-03-21 11:46:47 +01:00
|
|
|
else:
|
2016-05-18 11:56:46 +02:00
|
|
|
# Going down, keep track of where we were
|
2016-05-18 14:35:24 +02:00
|
|
|
if isinstance(node, ProjectNode):
|
|
|
|
self.project_name = node['name']
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
self.current_path /= node['_id']
|
|
|
|
self.log.debug('Going down to %r', self.current_path)
|
|
|
|
self.path_stack.append(node)
|
2016-03-21 11:46:47 +01:00
|
|
|
|
|
|
|
self.browse_assets()
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
@property
|
|
|
|
def node(self):
|
|
|
|
if not self.path_stack:
|
|
|
|
return None
|
|
|
|
return self.path_stack[-1]
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
def _finish(self, context):
|
2016-03-15 13:47:21 +01:00
|
|
|
self.log.debug('Finishing the modal operator')
|
2016-06-16 16:33:05 +02:00
|
|
|
async_loop.AsyncModalOperatorMixin._finish(self, context)
|
2016-03-22 14:35:33 +01:00
|
|
|
self.clear_images()
|
|
|
|
|
2016-03-15 17:09:27 +01:00
|
|
|
context.space_data.draw_handler_remove(self._draw_handle, 'WINDOW')
|
2016-05-10 14:33:02 +02:00
|
|
|
context.window.cursor_modal_restore()
|
2016-03-15 17:09:27 +01:00
|
|
|
|
|
|
|
if self.maximized_area:
|
|
|
|
bpy.ops.screen.screen_full_area(use_hide_panels=True)
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
context.area.tag_redraw()
|
2016-03-15 13:47:21 +01:00
|
|
|
self.log.debug('Modal operator finished')
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
def clear_images(self):
|
|
|
|
"""Removes all images we loaded from Blender's memory."""
|
|
|
|
|
|
|
|
for image in bpy.data.images:
|
|
|
|
if image.filepath_raw not in self.loaded_images:
|
|
|
|
continue
|
|
|
|
|
|
|
|
image.user_clear()
|
|
|
|
bpy.data.images.remove(image)
|
|
|
|
|
|
|
|
self.loaded_images.clear()
|
|
|
|
self.current_display_content.clear()
|
|
|
|
|
2016-03-14 18:12:29 +01:00
|
|
|
def add_menu_item(self, *args) -> MenuItem:
|
2016-03-14 17:23:56 +01:00
|
|
|
menu_item = MenuItem(*args)
|
|
|
|
|
|
|
|
# Just make this thread-safe to be on the safe side.
|
2016-03-14 18:12:29 +01:00
|
|
|
with self._menu_item_lock:
|
2016-03-14 17:23:56 +01:00
|
|
|
self.current_display_content.append(menu_item)
|
|
|
|
self.loaded_images.add(menu_item.icon.filepath_raw)
|
|
|
|
|
2016-05-18 14:54:49 +02:00
|
|
|
self.sort_menu()
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
return menu_item
|
|
|
|
|
2016-03-21 11:46:47 +01:00
|
|
|
def update_menu_item(self, node, *args) -> MenuItem:
|
|
|
|
node_uuid = node['_id']
|
|
|
|
|
2016-03-14 18:12:29 +01:00
|
|
|
# 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:
|
2016-03-21 11:46:47 +01:00
|
|
|
menu_item.update(node, *args)
|
2016-03-14 18:12:29 +01:00
|
|
|
self.loaded_images.add(menu_item.icon.filepath_raw)
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
raise ValueError('Unable to find MenuItem(node_uuid=%r)' % node_uuid)
|
|
|
|
|
2016-05-18 14:54:49 +02:00
|
|
|
self.sort_menu()
|
|
|
|
|
|
|
|
def sort_menu(self):
|
|
|
|
"""Sorts the self.current_display_content list."""
|
|
|
|
|
|
|
|
if not self.current_display_content:
|
|
|
|
return
|
|
|
|
|
|
|
|
with self._menu_item_lock:
|
|
|
|
self.current_display_content.sort(key=MenuItem.sort_key)
|
|
|
|
|
2016-05-04 14:30:47 +02:00
|
|
|
async def async_download_previews(self):
|
|
|
|
self._state = 'BROWSING'
|
|
|
|
|
|
|
|
thumbnails_directory = self.thumbnails_cache
|
2016-03-15 14:05:54 +01:00
|
|
|
self.log.info('Asynchronously downloading previews to %r', thumbnails_directory)
|
2016-05-18 11:56:46 +02:00
|
|
|
self.log.info('Current BCloud path is %r', self.current_path)
|
2016-03-14 17:23:56 +01:00
|
|
|
self.clear_images()
|
2016-07-15 17:01:24 +02:00
|
|
|
self._scroll_reset()
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-03-21 11:46:47 +01:00
|
|
|
def thumbnail_loading(node, texture_node):
|
|
|
|
self.add_menu_item(node, None, 'SPINNER', texture_node['name'])
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-03-21 11:46:47 +01:00
|
|
|
def thumbnail_loaded(node, file_desc, thumb_path):
|
|
|
|
self.update_menu_item(node, file_desc, thumb_path, file_desc['filename'])
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
project_uuid = self.current_path.project_uuid
|
|
|
|
node_uuid = self.current_path.node_uuid
|
|
|
|
|
|
|
|
if node_uuid:
|
2016-05-18 12:17:07 +02:00
|
|
|
# Query for sub-nodes of this node.
|
2016-05-18 11:56:46 +02:00
|
|
|
self.log.debug('Getting subnodes for parent node %r', node_uuid)
|
|
|
|
children = await pillar.get_nodes(parent_node_uuid=node_uuid,
|
2016-05-18 14:12:21 +02:00
|
|
|
node_type='group_texture')
|
2016-05-18 11:56:46 +02:00
|
|
|
elif project_uuid:
|
2016-05-18 12:17:07 +02:00
|
|
|
# Query for top-level nodes.
|
2016-05-18 11:56:46 +02:00
|
|
|
self.log.debug('Getting subnodes for project node %r', project_uuid)
|
2016-05-18 12:50:51 +02:00
|
|
|
children = await pillar.get_nodes(project_uuid=project_uuid,
|
|
|
|
parent_node_uuid='',
|
2016-05-18 14:12:21 +02:00
|
|
|
node_type='group_texture')
|
2016-03-14 17:23:56 +01:00
|
|
|
else:
|
2016-05-18 11:56:46 +02:00
|
|
|
# Query for projects
|
2016-05-19 12:31:22 +02:00
|
|
|
self.log.debug('No node UUID and no project UUID, listing available projects')
|
2016-05-18 11:56:46 +02:00
|
|
|
children = await pillar.get_texture_projects()
|
|
|
|
for proj_dict in children:
|
|
|
|
self.add_menu_item(ProjectNode(proj_dict), None, 'FOLDER', proj_dict['name'])
|
2016-03-21 11:46:47 +01:00
|
|
|
return
|
|
|
|
|
2016-05-18 12:17:07 +02:00
|
|
|
# Make sure we can go up again.
|
|
|
|
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
|
|
|
|
|
2016-03-21 11:46:47 +01:00
|
|
|
# Download all child nodes
|
2016-05-18 15:55:08 +02:00
|
|
|
self.log.debug('Iterating over child nodes of %r', self.current_path)
|
2016-03-21 11:46:47 +01:00
|
|
|
for child in children:
|
|
|
|
# print(' - %(_id)s = %(name)s' % child)
|
2016-05-17 17:30:57 +02:00
|
|
|
if child['node_type'] not in MenuItem.SUPPORTED_NODE_TYPES:
|
|
|
|
self.log.debug('Skipping node of type %r', child['node_type'])
|
|
|
|
continue
|
2016-03-21 11:46:47 +01:00
|
|
|
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.
|
2016-05-18 11:56:46 +02:00
|
|
|
if not node_uuid:
|
2016-03-21 11:46:47 +01:00
|
|
|
return
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
directory = os.path.join(thumbnails_directory, project_uuid, node_uuid)
|
2016-03-21 11:46:47 +01:00
|
|
|
os.makedirs(directory, exist_ok=True)
|
|
|
|
|
2016-05-18 11:56:46 +02:00
|
|
|
self.log.debug('Fetching texture thumbnails for node %r', node_uuid)
|
|
|
|
await pillar.fetch_texture_thumbs(node_uuid, 's', directory,
|
2016-03-21 11:46:47 +01:00
|
|
|
thumbnail_loading=thumbnail_loading,
|
|
|
|
thumbnail_loaded=thumbnail_loaded,
|
|
|
|
future=self.signalling_future)
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-03-15 14:05:54 +01:00
|
|
|
def browse_assets(self):
|
2016-05-18 11:56:46 +02:00
|
|
|
self.log.debug('Browsing assets at %r', self.current_path)
|
2016-05-04 14:30:47 +02:00
|
|
|
self._new_async_task(self.async_download_previews())
|
2016-03-15 15:44:19 +01:00
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
def draw_menu(self, context):
|
2016-03-15 15:44:19 +01:00
|
|
|
"""Draws the GUI with OpenGL."""
|
|
|
|
|
|
|
|
drawers = {
|
2016-05-04 14:30:47 +02:00
|
|
|
'CHECKING_CREDENTIALS': self._draw_checking_credentials,
|
2016-03-15 15:44:19 +01:00
|
|
|
'BROWSING': self._draw_browser,
|
|
|
|
'DOWNLOADING_TEXTURE': self._draw_downloading,
|
2016-03-23 13:46:19 +01:00
|
|
|
'EXCEPTION': self._draw_exception,
|
2016-05-10 14:52:51 +02:00
|
|
|
'PLEASE_SUBSCRIBE': self._draw_subscribe,
|
2016-03-15 15:44:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if self._state in drawers:
|
|
|
|
drawer = drawers[self._state]
|
|
|
|
drawer(context)
|
|
|
|
|
|
|
|
# For debugging: draw the state
|
|
|
|
font_id = 0
|
|
|
|
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
|
|
|
|
blf.size(font_id, 20, 72)
|
|
|
|
blf.position(font_id, 5, 5, 0)
|
2016-05-18 14:35:24 +02:00
|
|
|
blf.draw(font_id, '%s %s' % (self._state, self.project_name))
|
2016-03-15 15:44:19 +01:00
|
|
|
bgl.glDisable(bgl.GL_BLEND)
|
|
|
|
|
2016-03-15 17:09:27 +01:00
|
|
|
@staticmethod
|
|
|
|
def _window_region(context):
|
|
|
|
window_regions = [region
|
|
|
|
for region in context.area.regions
|
|
|
|
if region.type == 'WINDOW']
|
|
|
|
return window_regions[0]
|
|
|
|
|
2016-03-15 15:44:19 +01:00
|
|
|
def _draw_browser(self, context):
|
|
|
|
"""OpenGL drawing code for the BROWSING state."""
|
|
|
|
|
2016-03-15 17:09:27 +01:00
|
|
|
window_region = self._window_region(context)
|
2016-07-15 16:59:52 +02:00
|
|
|
content_width = window_region.width - ITEM_MARGIN_X * 2
|
|
|
|
content_height = window_region.height - ITEM_MARGIN_Y * 2
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-07-15 16:59:52 +02:00
|
|
|
content_x = ITEM_MARGIN_X
|
|
|
|
content_y = context.area.height - ITEM_MARGIN_Y - TARGET_ITEM_HEIGHT
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-07-15 16:59:52 +02:00
|
|
|
col_count = content_width // TARGET_ITEM_WIDTH
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-07-15 16:59:52 +02:00
|
|
|
item_width = (content_width - (col_count * ITEM_PADDING_X)) / col_count
|
|
|
|
item_height = TARGET_ITEM_HEIGHT
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-07-15 16:59:52 +02:00
|
|
|
block_width = item_width + ITEM_PADDING_X
|
|
|
|
block_height = item_height + ITEM_MARGIN_Y
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
bgl.glEnable(bgl.GL_BLEND)
|
|
|
|
bgl.glColor4f(0.0, 0.0, 0.0, 0.6)
|
2016-03-15 17:09:27 +01:00
|
|
|
bgl.glRectf(0, 0, window_region.width, window_region.height)
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
if self.current_display_content:
|
2016-07-15 17:01:24 +02:00
|
|
|
# The -1 / +2 are for extra rows that are drawn only half at the top/bottom.
|
|
|
|
first_item_idx = max(0, int(-self.scroll_offset // block_height - 1) * col_count)
|
|
|
|
items_per_page = int(content_height // item_height + 2) * col_count
|
|
|
|
last_item_idx = first_item_idx + items_per_page
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
for item_idx, item in enumerate(self.current_display_content):
|
2016-03-23 16:54:08 +01:00
|
|
|
x = content_x + (item_idx % col_count) * block_width
|
2016-07-15 17:01:24 +02:00
|
|
|
y = content_y - (item_idx // col_count) * block_height - self.scroll_offset
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
item.update_placement(x, y, item_width, item_height)
|
2016-07-15 17:01:24 +02:00
|
|
|
|
|
|
|
if first_item_idx <= item_idx < last_item_idx:
|
|
|
|
# Only draw if the item is actually on screen.
|
|
|
|
item.draw(highlighted=item.hits(self.mouse_x, self.mouse_y))
|
2016-03-14 17:23:56 +01:00
|
|
|
else:
|
|
|
|
font_id = 0
|
|
|
|
text = "Communicating with Blender Cloud"
|
|
|
|
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
|
|
|
|
blf.size(font_id, 20, 72)
|
|
|
|
text_width, text_height = blf.dimensions(font_id, text)
|
2016-03-23 16:54:08 +01:00
|
|
|
blf.position(font_id,
|
|
|
|
content_x + content_width * 0.5 - text_width * 0.5,
|
2016-03-14 17:23:56 +01:00
|
|
|
content_y - content_height * 0.3 + text_height * 0.5, 0)
|
|
|
|
blf.draw(font_id, text)
|
|
|
|
|
|
|
|
bgl.glDisable(bgl.GL_BLEND)
|
|
|
|
# bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
|
|
|
|
|
2016-03-15 15:44:19 +01:00
|
|
|
def _draw_downloading(self, context):
|
|
|
|
"""OpenGL drawing code for the DOWNLOADING_TEXTURE state."""
|
|
|
|
|
2016-05-04 14:30:47 +02:00
|
|
|
self._draw_text_on_colour(context,
|
|
|
|
'Downloading texture from Blender Cloud',
|
|
|
|
(0.0, 0.0, 0.2, 0.6))
|
|
|
|
|
|
|
|
def _draw_checking_credentials(self, context):
|
|
|
|
"""OpenGL drawing code for the CHECKING_CREDENTIALS state."""
|
2016-03-15 15:44:19 +01:00
|
|
|
|
2016-05-04 14:30:47 +02:00
|
|
|
self._draw_text_on_colour(context,
|
|
|
|
'Checking login credentials',
|
|
|
|
(0.0, 0.0, 0.2, 0.6))
|
|
|
|
|
|
|
|
def _draw_text_on_colour(self, context, text, bgcolour):
|
|
|
|
content_height, content_width = self._window_size(context)
|
2016-03-15 15:44:19 +01:00
|
|
|
bgl.glEnable(bgl.GL_BLEND)
|
2016-05-04 14:30:47 +02:00
|
|
|
bgl.glColor4f(*bgcolour)
|
2016-03-15 15:44:19 +01:00
|
|
|
bgl.glRectf(0, 0, content_width, content_height)
|
|
|
|
|
|
|
|
font_id = 0
|
|
|
|
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
|
|
|
|
blf.size(font_id, 20, 72)
|
|
|
|
text_width, text_height = blf.dimensions(font_id, text)
|
|
|
|
|
|
|
|
blf.position(font_id,
|
|
|
|
content_width * 0.5 - text_width * 0.5,
|
|
|
|
content_height * 0.7 + text_height * 0.5, 0)
|
|
|
|
blf.draw(font_id, text)
|
|
|
|
bgl.glDisable(bgl.GL_BLEND)
|
|
|
|
|
2016-03-23 13:46:19 +01:00
|
|
|
def _window_size(self, context):
|
|
|
|
window_region = self._window_region(context)
|
|
|
|
content_width = window_region.width
|
|
|
|
content_height = window_region.height
|
|
|
|
return content_height, content_width
|
|
|
|
|
|
|
|
def _draw_exception(self, context):
|
|
|
|
"""OpenGL drawing code for the EXCEPTION state."""
|
|
|
|
|
|
|
|
import textwrap
|
|
|
|
|
|
|
|
content_height, content_width = self._window_size(context)
|
|
|
|
|
|
|
|
bgl.glEnable(bgl.GL_BLEND)
|
|
|
|
bgl.glColor4f(0.2, 0.0, 0.0, 0.6)
|
|
|
|
bgl.glRectf(0, 0, content_width, content_height)
|
|
|
|
|
|
|
|
font_id = 0
|
2016-04-01 14:11:30 +02:00
|
|
|
ex = self.async_task.exception()
|
|
|
|
if isinstance(ex, pillar.UserNotLoggedInError):
|
|
|
|
ex_msg = 'You are not logged in on Blender ID. Please log in at User Preferences, ' \
|
|
|
|
'System, Blender ID.'
|
|
|
|
else:
|
|
|
|
ex_msg = str(ex)
|
|
|
|
if not ex_msg:
|
|
|
|
ex_msg = str(type(ex))
|
|
|
|
text = "An error occurred:\n%s" % ex_msg
|
2016-03-23 13:46:19 +01:00
|
|
|
lines = textwrap.wrap(text)
|
|
|
|
|
|
|
|
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
|
|
|
|
blf.size(font_id, 20, 72)
|
|
|
|
_, text_height = blf.dimensions(font_id, 'yhBp')
|
|
|
|
|
|
|
|
def position(line_nr):
|
|
|
|
blf.position(font_id,
|
|
|
|
content_width * 0.1,
|
|
|
|
content_height * 0.8 - line_nr * text_height, 0)
|
|
|
|
|
|
|
|
for line_idx, line in enumerate(lines):
|
|
|
|
position(line_idx)
|
|
|
|
blf.draw(font_id, line)
|
|
|
|
bgl.glDisable(bgl.GL_BLEND)
|
|
|
|
|
2016-05-10 14:52:51 +02:00
|
|
|
def _draw_subscribe(self, context):
|
|
|
|
self._draw_text_on_colour(context,
|
|
|
|
'Click to subscribe to the Blender Cloud',
|
|
|
|
(0.0, 0.0, 0.2, 0.6))
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
def get_clicked(self) -> MenuItem:
|
|
|
|
|
|
|
|
for item in self.current_display_content:
|
|
|
|
if item.hits(self.mouse_x, self.mouse_y):
|
|
|
|
return item
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2016-03-15 15:44:19 +01:00
|
|
|
def handle_item_selection(self, context, item: MenuItem):
|
2016-03-14 17:23:56 +01:00
|
|
|
"""Called when the user clicks on a menu item that doesn't represent a folder."""
|
2016-03-15 15:44:19 +01:00
|
|
|
|
2016-05-18 14:35:24 +02:00
|
|
|
from pillarsdk.utils import sanitize_filename
|
|
|
|
|
2016-03-22 13:26:24 +01:00
|
|
|
self.clear_images()
|
2016-03-15 15:44:19 +01:00
|
|
|
self._state = 'DOWNLOADING_TEXTURE'
|
2016-03-22 13:26:24 +01:00
|
|
|
|
2016-05-18 14:35:24 +02:00
|
|
|
node_path_components = (node['name'] for node in self.path_stack if node is not None)
|
|
|
|
local_path_components = [sanitize_filename(comp) for comp in node_path_components]
|
2016-03-22 13:26:24 +01:00
|
|
|
|
2016-03-31 11:54:15 +02:00
|
|
|
top_texture_directory = bpy.path.abspath(context.scene.local_texture_dir)
|
2016-03-22 13:26:24 +01:00
|
|
|
local_path = os.path.join(top_texture_directory, *local_path_components)
|
|
|
|
meta_path = os.path.join(top_texture_directory, '.blender_cloud')
|
|
|
|
|
|
|
|
self.log.info('Downloading texture %r to %s', item.node_uuid, local_path)
|
|
|
|
self.log.debug('Metadata will be stored at %s', meta_path)
|
|
|
|
|
|
|
|
file_paths = []
|
|
|
|
|
|
|
|
def texture_downloading(file_path, file_desc, *args):
|
|
|
|
self.log.info('Texture downloading to %s', file_path)
|
|
|
|
|
|
|
|
def texture_downloaded(file_path, file_desc, *args):
|
|
|
|
self.log.info('Texture downloaded to %r.', file_path)
|
2016-03-22 14:49:03 +01:00
|
|
|
image_dblock = bpy.data.images.load(filepath=file_path)
|
|
|
|
image_dblock['bcloud_file_uuid'] = file_desc['_id']
|
|
|
|
image_dblock['bcloud_texture_node_uuid'] = item.node_uuid
|
2016-03-22 13:26:24 +01:00
|
|
|
file_paths.append(file_path)
|
2016-03-15 15:44:19 +01:00
|
|
|
|
|
|
|
def texture_download_completed(_):
|
2016-03-22 13:26:24 +01:00
|
|
|
self.log.info('Texture download complete, inspect:\n%s', '\n'.join(file_paths))
|
2016-03-15 15:44:19 +01:00
|
|
|
self._state = 'QUIT'
|
|
|
|
|
2016-03-22 13:26:24 +01:00
|
|
|
signalling_future = asyncio.Future()
|
|
|
|
self._new_async_task(pillar.download_texture(item.node, local_path,
|
|
|
|
metadata_directory=meta_path,
|
|
|
|
texture_loading=texture_downloading,
|
|
|
|
texture_loaded=texture_downloaded,
|
|
|
|
future=signalling_future))
|
2016-03-15 15:44:19 +01:00
|
|
|
self.async_task.add_done_callback(texture_download_completed)
|
2016-03-14 17:23:56 +01:00
|
|
|
|
2016-05-10 14:52:51 +02:00
|
|
|
def open_browser_subscribe(self):
|
|
|
|
import webbrowser
|
|
|
|
|
|
|
|
webbrowser.open_new_tab('https://cloud.blender.org/join')
|
|
|
|
|
|
|
|
self.report({'INFO'}, 'We just started a browser for you.')
|
|
|
|
|
2016-07-15 17:01:24 +02:00
|
|
|
def _scroll_smooth(self):
|
|
|
|
diff = self.scroll_offset_target - self.scroll_offset
|
|
|
|
if diff == 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
if abs(round(diff)) < 1:
|
|
|
|
self.scroll_offset = self.scroll_offset_target
|
|
|
|
return
|
|
|
|
|
|
|
|
self.scroll_offset += diff * 0.5
|
|
|
|
|
|
|
|
def _scroll_by(self, amount):
|
|
|
|
self.scroll_offset_target = min(0, self.scroll_offset_target + amount)
|
|
|
|
|
|
|
|
def _scroll_reset(self):
|
|
|
|
self.scroll_offset_target = self.scroll_offset = 0
|
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
# store keymaps here to access after registration
|
|
|
|
addon_keymaps = []
|
|
|
|
|
|
|
|
|
|
|
|
def menu_draw(self, context):
|
|
|
|
layout = self.layout
|
|
|
|
layout.separator()
|
|
|
|
layout.operator(BlenderCloudBrowser.bl_idname, icon='MOD_SCREW')
|
|
|
|
|
|
|
|
|
|
|
|
def register():
|
|
|
|
bpy.utils.register_class(BlenderCloudBrowser)
|
2016-05-03 18:30:06 +02:00
|
|
|
# bpy.types.INFO_MT_mesh_add.append(menu_draw)
|
2016-03-14 17:23:56 +01:00
|
|
|
|
|
|
|
# handle the keymap
|
|
|
|
wm = bpy.context.window_manager
|
|
|
|
kc = wm.keyconfigs.addon
|
|
|
|
if not kc:
|
|
|
|
print('No addon key configuration space found, so no custom hotkeys added.')
|
|
|
|
return
|
|
|
|
|
2016-03-15 17:09:27 +01:00
|
|
|
km = kc.keymaps.new(name='Screen')
|
2016-03-14 17:23:56 +01:00
|
|
|
kmi = km.keymap_items.new('pillar.browser', 'A', 'PRESS', ctrl=True, shift=True, alt=True)
|
|
|
|
addon_keymaps.append((km, kmi))
|
|
|
|
|
|
|
|
|
|
|
|
def unregister():
|
|
|
|
# handle the keymap
|
|
|
|
for km, kmi in addon_keymaps:
|
|
|
|
km.keymap_items.remove(kmi)
|
|
|
|
addon_keymaps.clear()
|
|
|
|
|
2016-07-15 14:27:42 +02:00
|
|
|
bpy.utils.unregister_class(BlenderCloudBrowser)
|