Compare commits

...

16 Commits

Author SHA1 Message Date
e300c32d64 Bumped version to 1.3.3 2016-07-20 11:13:31 +02:00
63eaaf7dc9 Added addon-bundle dir 2016-07-20 10:59:17 +02:00
6fcea9469f Limit scrolling to content area. 2016-07-19 18:13:32 +02:00
61f86d63e0 Scrolling on MacOS X 2016-07-19 18:13:18 +02:00
0d69b1d7ec Removed trailing period from bl_desc 2016-07-19 18:13:09 +02:00
d5139c767e Texture browser: Added scrolling.
You can scroll indefinitely for now. Might fix that in a later commit.
2016-07-15 17:01:24 +02:00
f0d829da49 Renamed some constants to all-caps 2016-07-15 16:59:52 +02:00
a4817259c8 Moved import 2016-07-15 16:56:55 +02:00
f899f6d1ab Started pagination support, but it isn't used yet. 2016-07-15 16:56:39 +02:00
9a0873eea4 Renamed gui.py to texture_browser.py
Also discovered double-unregister of a class, so that fixed an old bug.
Removed the workaround for that bug.
2016-07-15 14:27:42 +02:00
388a059400 Bumped version to 1.3.2 2016-07-15 14:02:01 +02:00
80d2b5b2e7 Move "Share on Cloud" button from image header to menu. 2016-07-15 14:01:21 +02:00
53ab2fc6df Bumped version to 1.3.1 2016-07-14 11:50:19 +02:00
1e2c74e82d Made screenshot the default target for image sharing.
This way the spacebar-menu takes a screenshot of the current area and
shares it. The other targets need a 'name' property set, so those won't
work from the spacebar-menu anyway.

I also added some extra options for the screenshotting, to mirror the
bpy.ops.screen.screenshot() operator options.

The full-window screenshot operator is now also placed in the Window menu.
2016-07-14 11:49:30 +02:00
ecb8f8575f Added missing logger 2016-07-14 11:47:50 +02:00
acd62b4917 Added screenshot functionality 2016-07-14 11:13:09 +02:00
9 changed files with 250 additions and 55 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ blender_cloud/wheels/*.whl
/test_*.py /test_*.py
/dist/ /dist/
/build/ /build/
/addon-bundle/*.zip

52
addon-bundle/README.txt Normal file
View File

@@ -0,0 +1,52 @@
Blender Cloud Addon
===================
Congratulations on downloading the Blender Cloud addon. For your
convenience, we have bundled it with the Blender ID addon.
To use the Blender Cloud addon, perform the following steps:
- Use Blender (File, User Preferences, Addons, Install from file)
to install blender_id-x.x.x.addon.zip
- If you had a previous version of the Blender Cloud addon installed,
restart Blender now.
- Log in with your Blender ID.
- Use Blender to install blender_cloud-x.x.x.addon.zip
If you don't see the addon in the list, enable the Testing
category.
- Press Ctrl+Alt+Shift+A to start the texture browser.
- Visit the User Preferences, Addons panel, to use the Blender Sync
feature.
Support for Blenders not from blender.org
-----------------------------------------
Maybe you use Blender from another source than blender.org, such as an
Ubuntu package. If that is the case, you have to make sure that the
Python package "requests" is installed. On Ubuntu Linux this can be
done with the command
sudo apt-get install python3-requests
On other platforms & distributions this might be different.
Blender uses Python 3.5, so make sure you install the package for the
correct version of Python.
Subscribing to the Blender Cloud
--------------------------------
The Blender Sync feature is free to use for everybody with a Blender
ID account. In order to use the Texture Browser you need to have a
Blender Cloud subscription. If you didn't subscribe yet, go to:
https://cloud.blender.org/join

16
addon-bundle/bundle.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
cd $(dirname $(readlink -f $0))
BCLOUD=$(ls ../dist/blender_cloud-*.addon.zip | tail -n 1)
BID=$(ls ../../../blender-id-addon/dist/blender_id-*.addon.zip | tail -n 1)
cp -va $BCLOUD $BID .
BUNDLE=$(basename $BCLOUD)
BUNDLE=${BUNDLE/.addon.zip/-bundle-UNZIP_ME_FIRST.zip}
zip -9 $BUNDLE $(basename $BCLOUD) $(basename $BID) README.txt
dolphin --select $BUNDLE 2>/dev/null >/dev/null & disown
echo "CREATED: $BUNDLE"

View File

@@ -21,7 +21,7 @@
bl_info = { bl_info = {
'name': 'Blender Cloud', 'name': 'Blender Cloud',
'author': 'Sybren A. Stüvel and Francesco Siddi', 'author': 'Sybren A. Stüvel and Francesco Siddi',
'version': (1, 3, 0), 'version': (1, 3, 3),
'blender': (2, 77, 0), 'blender': (2, 77, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser', 'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon ' 'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '
@@ -74,18 +74,18 @@ def register():
reload_mod('home_project') reload_mod('home_project')
blender = reload_mod('blender') blender = reload_mod('blender')
gui = reload_mod('gui')
async_loop = reload_mod('async_loop') async_loop = reload_mod('async_loop')
texture_browser = reload_mod('texture_browser')
settings_sync = reload_mod('settings_sync') settings_sync = reload_mod('settings_sync')
image_sharing = reload_mod('image_sharing') image_sharing = reload_mod('image_sharing')
else: else:
from . import (blender, gui, async_loop, settings_sync, blendfile, home_project, from . import (blender, texture_browser, async_loop, settings_sync, blendfile, home_project,
image_sharing) image_sharing)
async_loop.setup_asyncio_executor() async_loop.setup_asyncio_executor()
async_loop.register() async_loop.register()
gui.register() texture_browser.register()
blender.register() blender.register()
settings_sync.register() settings_sync.register()
image_sharing.register() image_sharing.register()
@@ -109,10 +109,10 @@ def _monkey_patch_requests():
def unregister(): def unregister():
from . import blender, gui, async_loop, settings_sync, image_sharing from . import blender, texture_browser, async_loop, settings_sync, image_sharing
image_sharing.unregister() image_sharing.unregister()
settings_sync.unregister() settings_sync.unregister()
blender.unregister() blender.unregister()
gui.unregister() texture_browser.unregister()
async_loop.unregister() async_loop.unregister()

View File

@@ -10,7 +10,7 @@ import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty
from . import pillar, gui from . import pillar
PILLAR_SERVER_URL = 'https://cloudapi.blender.org/' PILLAR_SERVER_URL = 'https://cloudapi.blender.org/'
# PILLAR_SERVER_URL = 'http://localhost:5000/' # PILLAR_SERVER_URL = 'http://localhost:5000/'
@@ -41,7 +41,7 @@ class SyncStatusProperties(PropertyGroup):
('SYNCING', 'SYNCING', 'Synchronising with Blender Cloud.'), ('SYNCING', 'SYNCING', 'Synchronising with Blender Cloud.'),
], ],
name='status', name='status',
description='Current status of Blender Sync.', description='Current status of Blender Sync',
update=redraw) update=redraw)
version = EnumProperty( version = EnumProperty(
@@ -230,6 +230,8 @@ class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
bl_idname = 'pillar.credentials_update' bl_idname = 'pillar.credentials_update'
bl_label = 'Update credentials' bl_label = 'Update credentials'
log = logging.getLogger('bpy.ops.%s' % bl_idname)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
# Only allow activation when the user is actually logged in. # Only allow activation when the user is actually logged in.
@@ -352,8 +354,6 @@ def register():
def unregister(): def unregister():
unload_custom_icons() unload_custom_icons()
gui.unregister()
bpy.utils.unregister_class(PillarCredentialsUpdate) bpy.utils.unregister_class(PillarCredentialsUpdate)
bpy.utils.unregister_class(BlenderCloudPreferences) bpy.utils.unregister_class(BlenderCloudPreferences)
bpy.utils.unregister_class(SyncStatusProperties) bpy.utils.unregister_class(SyncStatusProperties)

View File

@@ -1,6 +1,7 @@
import logging import logging
import os.path import os.path
import tempfile import tempfile
import datetime
import bpy import bpy
import pillarsdk import pillarsdk
@@ -38,7 +39,7 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
async_loop.AsyncModalOperatorMixin, async_loop.AsyncModalOperatorMixin,
bpy.types.Operator): bpy.types.Operator):
bl_idname = 'pillar.image_share' bl_idname = 'pillar.image_share'
bl_label = 'Share an image via Blender Cloud' bl_label = 'Share an image/screenshot via Blender Cloud'
bl_description = 'Uploads an image for sharing via Blender Cloud' bl_description = 'Uploads an image for sharing via Blender Cloud'
log = logging.getLogger('bpy.ops.%s' % bl_idname) log = logging.getLogger('bpy.ops.%s' % bl_idname)
@@ -50,19 +51,39 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
target = bpy.props.EnumProperty( target = bpy.props.EnumProperty(
items=[ items=[
('FILE', 'File', 'Upload an image file'), ('FILE', 'File', 'Share an image file'),
('DATABLOCK', 'Datablock', 'Upload an image datablock'), ('DATABLOCK', 'Datablock', 'Share an image datablock'),
('SCREENSHOT', 'Screenshot', 'Share a screenshot'),
], ],
name='target', name='target',
default='DATABLOCK') default='SCREENSHOT')
name = bpy.props.StringProperty(name='name', name = bpy.props.StringProperty(name='name',
description='File or datablock name to sync') description='File or datablock name to sync')
screenshot_show_multiview = bpy.props.BoolProperty(
name='screenshot_show_multiview',
description='Enable Multi-View',
default=False)
screenshot_use_multiview = bpy.props.BoolProperty(
name='screenshot_use_multiview',
description='Use Multi-View',
default=False)
screenshot_full = bpy.props.BoolProperty(
name='screenshot_full',
description='Full Screen, Capture the whole window (otherwise only capture the active area)',
default=False)
def invoke(self, context, event): def invoke(self, context, event):
# Do a quick test on datablock dirtyness. If it's not packed and dirty, # Do a quick test on datablock dirtyness. If it's not packed and dirty,
# the user should save it first. # the user should save it first.
if self.target == 'DATABLOCK': if self.target == 'DATABLOCK':
if not self.name:
self.report({'ERROR'}, 'No name given of the datablock to share.')
return {'CANCELLED'}
datablock = bpy.data.images[self.name] datablock = bpy.data.images[self.name]
if datablock.type == 'IMAGE' and datablock.is_dirty and not datablock.packed_file: if datablock.type == 'IMAGE' and datablock.is_dirty and not datablock.packed_file:
self.report({'ERROR'}, 'Datablock is dirty, save it first.') self.report({'ERROR'}, 'Datablock is dirty, save it first.')
@@ -138,10 +159,13 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
async def share_image(self, context): async def share_image(self, context):
"""Sends files to the Pillar server.""" """Sends files to the Pillar server."""
self.report({'INFO'}, "Uploading %s '%s'" % (self.target.lower(), self.name))
if self.target == 'FILE': if self.target == 'FILE':
self.report({'INFO'}, "Uploading %s '%s'" % (self.target.lower(), self.name))
node = await self.upload_file(self.name) node = await self.upload_file(self.name)
elif self.target == 'SCREENSHOT':
node = await self.upload_screenshot(context)
else: else:
self.report({'INFO'}, "Uploading %s '%s'" % (self.target.lower(), self.name))
node = await self.upload_datablock(context) node = await self.upload_datablock(context)
self.report({'INFO'}, 'Upload complete, creating link to share.') self.report({'INFO'}, 'Upload complete, creating link to share.')
@@ -245,6 +269,21 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
self.log.info('Uploading packed file directly from memory to %r.', filename) self.log.info('Uploading packed file directly from memory to %r.', filename)
return await self.upload_file(filename, fileobj=fileobj) return await self.upload_file(filename, fileobj=fileobj)
async def upload_screenshot(self, context) -> pillarsdk.Node:
"""Takes a screenshot, saves it to a temp file, and uploads it."""
self.name = datetime.datetime.now().strftime('Screenshot-%Y-%m-%d-%H:%M:%S.png')
self.report({'INFO'}, "Uploading %s '%s'" % (self.target.lower(), self.name))
with tempfile.TemporaryDirectory() as tmpdir:
filepath = os.path.join(tmpdir, self.name)
self.log.debug('Saving screenshot to %s', filepath)
bpy.ops.screen.screenshot(filepath=filepath,
show_multiview=self.screenshot_show_multiview,
use_multiview=self.screenshot_use_multiview,
full=self.screenshot_full)
return await self.upload_file(filepath)
def image_editor_menu(self, context): def image_editor_menu(self, context):
image = context.space_data.image image = context.space_data.image
@@ -262,13 +301,23 @@ def image_editor_menu(self, context):
props.name = image.name props.name = image.name
def window_menu(self, context):
props = self.layout.operator(PILLAR_OT_image_share.bl_idname,
text='Share screenshot via Blender Cloud',
icon_value=blender.icon('CLOUD'))
props.target = 'SCREENSHOT'
props.screenshot_full = True
def register(): def register():
bpy.utils.register_class(PILLAR_OT_image_share) bpy.utils.register_class(PILLAR_OT_image_share)
bpy.types.IMAGE_HT_header.append(image_editor_menu) bpy.types.IMAGE_MT_image.append(image_editor_menu)
bpy.types.INFO_MT_window.append(window_menu)
def unregister(): def unregister():
bpy.utils.unregister_class(PILLAR_OT_image_share) bpy.utils.unregister_class(PILLAR_OT_image_share)
bpy.types.IMAGE_HT_header.remove(image_editor_menu) bpy.types.IMAGE_MT_image.remove(image_editor_menu)
bpy.types.INFO_MT_window.remove(window_menu)

View File

@@ -260,7 +260,7 @@ async def get_project_uuid(project_url: str) -> str:
async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None, async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
node_type=None) -> list: node_type=None, max_results=None) -> list:
"""Gets nodes for either a project or given a parent node. """Gets nodes for either a project or given a parent node.
@param project_uuid: the UUID of the project, or None if only querying by parent_node_uuid. @param project_uuid: the UUID of the project, or None if only querying by parent_node_uuid.
@@ -290,23 +290,34 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
else: else:
where['node_type'] = {'$in': node_type} where['node_type'] = {'$in': node_type}
children = await pillar_call(pillarsdk.Node.all, { params = {'projection': {'name': 1, 'parent': 1, 'node_type': 1, 'properties.order': 1,
'projection': {'name': 1, 'parent': 1, 'node_type': 1, 'properties.status': 1, 'properties.files': 1,
'properties.order': 1, 'properties.status': 1,
'properties.files': 1,
'properties.content_type': 1, 'picture': 1}, 'properties.content_type': 1, 'picture': 1},
'where': where, 'where': where,
'embed': ['parent']}) 'embed': ['parent']}
# Pagination
if max_results:
params['max_results'] = int(max_results)
children = await pillar_call(pillarsdk.Node.all, params)
return children['_items'] return children['_items']
async def get_texture_projects() -> list: async def get_texture_projects(max_results=None) -> list:
"""Returns project dicts that contain textures.""" """Returns project dicts that contain textures."""
params = {}
# Pagination
if max_results:
params['max_results'] = int(max_results)
try: try:
children = await pillar_call(pillarsdk.Project.all_from_endpoint, children = await pillar_call(pillarsdk.Project.all_from_endpoint,
'/bcloud/texture-libraries') '/bcloud/texture-libraries',
params=params)
except pillarsdk.ResourceNotFound as ex: except pillarsdk.ResourceNotFound as ex:
log.warning('Unable to find texture projects: %s', ex) log.warning('Unable to find texture projects: %s', ex)
raise PillarError('Unable to find texture projects: %s' % ex) raise PillarError('Unable to find texture projects: %s' % ex)

View File

@@ -21,21 +21,25 @@
import asyncio import asyncio
import logging import logging
import threading import threading
import os
import bpy import bpy
import bgl import bgl
import blf import blf
import os
import pillarsdk import pillarsdk
from . import async_loop, pillar, cache from . import async_loop, pillar, cache
REQUIRED_ROLES_FOR_TEXTURE_BROWSER = {'subscriber', 'demo'} REQUIRED_ROLES_FOR_TEXTURE_BROWSER = {'subscriber', 'demo'}
MOUSE_SCROLL_PIXELS_PER_TICK = 50
icon_width = 128 ICON_WIDTH = 128
icon_height = 128 ICON_HEIGHT = 128
target_item_width = 400 TARGET_ITEM_WIDTH = 400
target_item_height = 128 TARGET_ITEM_HEIGHT = 128
ITEM_MARGIN_X = 5
ITEM_MARGIN_Y = 5
ITEM_PADDING_X = 5
library_path = '/tmp' 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")
@@ -179,11 +183,11 @@ class MenuItem:
bgl.glTexCoord2d(0, 0) bgl.glTexCoord2d(0, 0)
bgl.glVertex2d(self.x + self.icon_margin_x, self.y) bgl.glVertex2d(self.x + self.icon_margin_x, self.y)
bgl.glTexCoord2d(0, 1) bgl.glTexCoord2d(0, 1)
bgl.glVertex2d(self.x + self.icon_margin_x, self.y + icon_height) bgl.glVertex2d(self.x + self.icon_margin_x, self.y + ICON_HEIGHT)
bgl.glTexCoord2d(1, 1) bgl.glTexCoord2d(1, 1)
bgl.glVertex2d(self.x + self.icon_margin_x + icon_width, self.y + icon_height) bgl.glVertex2d(self.x + self.icon_margin_x + ICON_WIDTH, self.y + ICON_HEIGHT)
bgl.glTexCoord2d(1, 0) bgl.glTexCoord2d(1, 0)
bgl.glVertex2d(self.x + self.icon_margin_x + icon_width, self.y) bgl.glVertex2d(self.x + self.icon_margin_x + ICON_WIDTH, self.y)
bgl.glEnd() bgl.glEnd()
bgl.glDisable(bgl.GL_TEXTURE_2D) bgl.glDisable(bgl.GL_TEXTURE_2D)
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
@@ -193,8 +197,8 @@ class MenuItem:
# draw some text # draw some text
font_id = 0 font_id = 0
blf.position(font_id, blf.position(font_id,
self.x + self.icon_margin_x + icon_width + self.text_margin_x, self.x + self.icon_margin_x + ICON_WIDTH + self.text_margin_x,
self.y + icon_height * 0.5 - 0.25 * self.text_height, 0) self.y + ICON_HEIGHT * 0.5 - 0.25 * self.text_height, 0)
blf.size(font_id, self.text_height, self.text_width) blf.size(font_id, self.text_height, self.text_width)
blf.draw(font_id, self.label_text) blf.draw(font_id, self.label_text)
@@ -227,6 +231,10 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
mouse_x = 0 mouse_x = 0
mouse_y = 0 mouse_y = 0
scroll_offset = 0
scroll_offset_target = 0
scroll_offset_max = 0
scroll_offset_space_left = 0
def invoke(self, context, event): def invoke(self, context, event):
# Refuse to start if the file hasn't been saved. # Refuse to start if the file hasn't been saved.
@@ -256,6 +264,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.current_display_content = [] self.current_display_content = []
self.loaded_images = set() self.loaded_images = set()
self._scroll_reset()
context.window.cursor_modal_set('DEFAULT') context.window.cursor_modal_set('DEFAULT')
async_loop.AsyncModalOperatorMixin.invoke(self, context, event) async_loop.AsyncModalOperatorMixin.invoke(self, context, event)
@@ -273,6 +282,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
async_loop.ensure_async_loop() async_loop.ensure_async_loop()
if event.type == 'TIMER': if event.type == 'TIMER':
self._scroll_smooth()
context.area.tag_redraw() context.area.tag_redraw()
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
@@ -295,6 +305,18 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
else: else:
context.window.cursor_set('DEFAULT') context.window.cursor_set('DEFAULT')
# 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()
elif event.type == 'TRACKPADPAN':
self._scroll_by(event.mouse_prev_y - event.mouse_y,
smooth=False)
context.area.tag_redraw()
if left_mouse_release: if left_mouse_release:
if selected is None: if selected is None:
# No item clicked, ignore it. # No item clicked, ignore it.
@@ -442,6 +464,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
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.log.info('Current BCloud path is %r', self.current_path)
self.clear_images() self.clear_images()
self._scroll_reset()
def thumbnail_loading(node, texture_node): def thumbnail_loading(node, texture_node):
self.add_menu_item(node, None, 'SPINNER', texture_node['name']) self.add_menu_item(node, None, 'SPINNER', texture_node['name'])
@@ -534,36 +557,54 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
def _draw_browser(self, context): def _draw_browser(self, context):
"""OpenGL drawing code for the BROWSING state.""" """OpenGL drawing code for the BROWSING state."""
margin_x = 5
margin_y = 5
padding_x = 5
window_region = self._window_region(context) window_region = self._window_region(context)
content_width = window_region.width - margin_x * 2 content_width = window_region.width - ITEM_MARGIN_X * 2
content_height = window_region.height - margin_y * 2 content_height = window_region.height - ITEM_MARGIN_Y * 2
content_x = margin_x content_x = ITEM_MARGIN_X
content_y = context.area.height - margin_y - target_item_height content_y = context.area.height - ITEM_MARGIN_Y - TARGET_ITEM_HEIGHT
col_count = content_width // target_item_width col_count = content_width // TARGET_ITEM_WIDTH
item_width = (content_width - (col_count * padding_x)) / col_count item_width = (content_width - (col_count * ITEM_PADDING_X)) / col_count
item_height = target_item_height item_height = TARGET_ITEM_HEIGHT
block_width = item_width + padding_x block_width = item_width + ITEM_PADDING_X
block_height = item_height + margin_y block_height = item_height + ITEM_MARGIN_Y
bgl.glEnable(bgl.GL_BLEND) bgl.glEnable(bgl.GL_BLEND)
bgl.glColor4f(0.0, 0.0, 0.0, 0.6) bgl.glColor4f(0.0, 0.0, 0.0, 0.6)
bgl.glRectf(0, 0, window_region.width, window_region.height) bgl.glRectf(0, 0, window_region.width, window_region.height)
if self.current_display_content: if self.current_display_content:
bottom_y = float('inf')
# 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
for item_idx, item in enumerate(self.current_display_content): for item_idx, item in enumerate(self.current_display_content):
x = content_x + (item_idx % col_count) * block_width x = content_x + (item_idx % col_count) * block_width
y = content_y - (item_idx // col_count) * block_height y = content_y - (item_idx // col_count) * block_height - self.scroll_offset
item.update_placement(x, y, item_width, item_height) item.update_placement(x, y, item_width, item_height)
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)) item.draw(highlighted=item.hits(self.mouse_x, self.mouse_y))
bottom_y = min(y, bottom_y)
bgl.glColor4f(0.24, 0.68, 0.91, 1)
bgl.glRectf(0,
bottom_y - ITEM_MARGIN_Y,
window_region.width,
bottom_y+1 - ITEM_MARGIN_Y)
self.scroll_offset_space_left = window_region.height - bottom_y
self.scroll_offset_max = (self.scroll_offset -
self.scroll_offset_space_left +
0.25 * block_height)
else: else:
font_id = 0 font_id = 0
text = "Communicating with Blender Cloud" text = "Communicating with Blender Cloud"
@@ -714,6 +755,32 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.report({'INFO'}, 'We just started a browser for you.') self.report({'INFO'}, 'We just started a browser for you.')
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, *, smooth=True):
# Slow down scrolling up
if smooth and amount < 0 and -amount > self.scroll_offset_space_left / 4:
amount = -self.scroll_offset_space_left / 4
self.scroll_offset_target = min(0,
max(self.scroll_offset_max,
self.scroll_offset_target + amount))
if not smooth:
self._scroll_offset = self.scroll_offset_target
def _scroll_reset(self):
self.scroll_offset_target = self.scroll_offset = 0
# store keymaps here to access after registration # store keymaps here to access after registration
addon_keymaps = [] addon_keymaps = []
@@ -747,5 +814,4 @@ def unregister():
km.keymap_items.remove(kmi) km.keymap_items.remove(kmi)
addon_keymaps.clear() addon_keymaps.clear()
if 'bl_rna' in BlenderCloudBrowser.__dict__: # <-- check if we already removed!
bpy.utils.unregister_class(BlenderCloudBrowser) bpy.utils.unregister_class(BlenderCloudBrowser)

View File

@@ -179,7 +179,7 @@ setup(
'wheels': BuildWheels}, 'wheels': BuildWheels},
name='blender_cloud', name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.', description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.3.0', version='1.3.3',
author='Sybren A. Stüvel', author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu', author_email='sybren@stuvel.eu',
packages=find_packages('.'), packages=find_packages('.'),