Compare commits

...

28 Commits

Author SHA1 Message Date
abcd8b0168 Added description for PillarCredentialsUpdate operator. 2016-10-04 16:44:15 +02:00
534a5a6ac4 Attract project refresh: Show report when credential checking fails 2016-10-04 16:42:13 +02:00
6b5faa423e Bumped pillarsdk requirement to 1.6.0 2016-10-04 16:27:28 +02:00
232e8f6167 Bumped version to 1.4.99 (because Blender doesn't do beta versions)
The bl_info['version'] value is just a tuple of numbers, so there is no
room for beta version info. I did add a warning, though.
2016-10-04 16:15:40 +02:00
af0dee0c9d Make attract.strip_unlink unlink all selected strips, and not just the active 2016-09-30 13:58:49 +02:00
baac86f59b New shots should start with 'todo' status.
In a later version this default status should either be set server-side
and returned in the POST response, or be taken from the project definition
by this add-on.
2016-09-30 13:58:31 +02:00
19d54b7fd6 Don't filter available projects on is_private=True. 2016-09-29 18:54:50 +02:00
0067157251 Dim status line for strips with atc_is_synced == False 2016-09-27 16:53:43 +02:00
4c84fbf339 Fixed strip drawing.
It didn't consider meta-strips, we look at that now. Also the line is
thinner and uses the same colours as the web interface to indicate strip
status.
2016-09-27 16:51:46 +02:00
6a5c392b5b Lots of Attract tweaks 2016-09-27 16:26:46 +02:00
cbaccaed49 Different node type name 2016-09-27 15:55:39 +02:00
cfc53e007c Removed more atc_cut_in/out 2016-09-27 15:51:24 +02:00
2768f0a59f Added refreshing data that is determined by web interface. 2016-09-27 15:43:52 +02:00
b6c7ec1546 Removed unused Attract properties 2016-09-27 15:43:35 +02:00
417b6e80f5 Working on Attract integration 2016-09-23 17:45:06 +02:00
90259297ca Use project UUID from new property in preferences 2016-09-23 14:06:36 +02:00
3d9f4e893a Store attract project in preferences (instead of windowmanager)
This actually saves the available projects, allowing a refresh when needed.
2016-09-23 14:06:36 +02:00
4be497ed27 Added project selector for Attract. 2016-09-23 14:06:36 +02:00
28fe6e8f96 pillar.call → pillar.sync_call 2016-09-23 14:06:36 +02:00
22e4f2dc5e Some cosmetic changes 2016-09-23 14:06:36 +02:00
537dcf846a No need to manually compute frame_final_start 2016-09-23 14:06:36 +02:00
8ca4159fe8 Sync shot notes & description 2016-09-23 14:06:36 +02:00
d7bf001ffe Show cut-out frame nr as read-only property. 2016-09-23 14:06:36 +02:00
6ea15d2bfe No need to manually keep track of index. 2016-09-23 14:06:36 +02:00
6fda496652 Attract sequence strip buttons are working. 2016-09-23 14:06:36 +02:00
8dab01138e More work on attract integration 2016-09-23 14:06:36 +02:00
3da76ddb24 Added proper error messages for when the project needs Attract setup.
When the 'shot' node type doesn't exist, we now show an error message
about this (instead of causing an IndexError).
2016-09-23 14:06:36 +02:00
c57da7ab2b WIP: integration of the Attract addon into the Blender Cloud adddon. 2016-09-23 14:06:36 +02:00
7 changed files with 757 additions and 8 deletions

View File

@@ -20,8 +20,8 @@
bl_info = {
'name': 'Blender Cloud',
'author': 'Sybren A. Stüvel and Francesco Siddi',
'version': (1, 4, 3),
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 4, 99),
'blender': (2, 77, 0),
'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 '
@@ -29,6 +29,7 @@ bl_info = {
'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.6/Py/'
'Scripts/System/BlenderCloud',
'category': 'System',
'warning': 'This is a beta version; the first to support Attract.'
}
import logging
@@ -78,9 +79,10 @@ def register():
texture_browser = reload_mod('texture_browser')
settings_sync = reload_mod('settings_sync')
image_sharing = reload_mod('image_sharing')
attract = reload_mod('attract')
else:
from . import (blender, texture_browser, async_loop, settings_sync, blendfile, home_project,
image_sharing)
image_sharing, attract)
async_loop.setup_asyncio_executor()
async_loop.register()
@@ -89,6 +91,7 @@ def register():
blender.register()
settings_sync.register()
image_sharing.register()
attract.register()
def _monkey_patch_requests():
@@ -109,9 +112,10 @@ def _monkey_patch_requests():
def unregister():
from . import blender, texture_browser, async_loop, settings_sync, image_sharing
from . import blender, texture_browser, async_loop, settings_sync, image_sharing, attract
image_sharing.unregister()
attract.unregister()
settings_sync.unregister()
blender.unregister()
texture_browser.unregister()

View File

@@ -0,0 +1,446 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# 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 2
# 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
# Old info, kept here for reference, so that we can merge wiki pages,
# descriptions, etc.
#
# bl_info = {
# "name": "Attract",
# "author": "Francesco Siddi, Inês Almeida, Antony Riakiotakis",
# "version": (0, 2, 0),
# "blender": (2, 76, 0),
# "location": "Video Sequence Editor",
# "description":
# "Blender integration with the Attract task tracking service"
# ". *requires the Blender ID add-on",
# "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
# "Scripts/Workflow/Attract",
# "category": "Workflow",
# "support": "TESTING"
# }
import functools
import logging
if "bpy" in locals():
import importlib
importlib.reload(draw)
else:
from . import draw
import bpy
from pillarsdk.nodes import Node
from pillarsdk.projects import Project
from pillarsdk import exceptions as sdk_exceptions
from bpy.types import Operator, Panel, AddonPreferences
log = logging.getLogger(__name__)
def active_strip(context):
try:
return context.scene.sequence_editor.active_strip
except AttributeError:
return None
def remove_atc_props(strip):
"""Resets the attract custom properties assigned to a VSE strip"""
strip.atc_name = ""
strip.atc_description = ""
strip.atc_object_id = ""
strip.atc_is_synced = False
class ToolsPanel(Panel):
bl_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI'
def draw_header(self, context):
strip = active_strip(context)
if strip and strip.atc_object_id:
self.layout.prop(strip, 'atc_is_synced', text='')
def draw(self, context):
strip = active_strip(context)
layout = self.layout
strip_types = {'MOVIE', 'IMAGE'}
if strip and strip.atc_object_id and strip.type in strip_types:
layout.prop(strip, 'atc_name', text='Name')
layout.prop(strip, 'atc_status', text='Status')
# Create a special sub-layout for read-only properties.
ro_sub = layout.column(align=True)
ro_sub.enabled = False
ro_sub.prop(strip, 'atc_description', text='Description')
ro_sub.prop(strip, 'atc_notes', text='Notes')
if strip.atc_is_synced:
row = layout.row(align=True)
row.operator('attract.shot_submit_update')
row.operator(AttractShotFetchUpdate.bl_idname,
text='', icon='FILE_REFRESH')
# Group more dangerous operations.
dangerous_sub = layout.column(align=True)
dangerous_sub.operator('attract.shot_delete')
dangerous_sub.operator('attract.strip_unlink')
elif strip and strip.type in strip_types:
layout.operator('attract.shot_submit_new')
layout.operator('attract.shot_relink')
else:
layout.label(text='Select a Movie or Image strip')
layout.operator(AttractShotSubmitSelected.bl_idname)
class AttractOperatorMixin:
"""Mix-in class for all Attract operators."""
def _project_needs_setup_error(self):
self.report({'ERROR'}, 'Your Blender Cloud project is not set up for Attract.')
return {'CANCELLED'}
@functools.lru_cache()
def find_project(self, project_uuid: str) -> Project:
"""Finds a single project.
Caches the result in memory to prevent more than one call to Pillar.
"""
from .. import pillar
project = pillar.sync_call(Project.find_one, {'where': {'_id': project_uuid}})
return project
def find_node_type(self, node_type_name: str) -> dict:
from .. import pillar, blender
prefs = blender.preferences()
project = self.find_project(prefs.attract_project.project)
# FIXME: Eve doesn't seem to handle the $elemMatch projection properly,
# even though it works fine in MongoDB itself. As a result, we have to
# search for the node type.
node_type_list = project['node_types']
node_type = next((nt for nt in node_type_list if nt['name'] == node_type_name), None)
if not node_type:
return self._project_needs_setup_error()
return node_type
def submit_new_strip(self, strip):
from .. import pillar, blender
# Define the shot properties
user_uuid = pillar.pillar_user_uuid()
if not user_uuid:
self.report({'ERROR'}, 'Your Blender Cloud user ID is not known, '
'update your credentials.')
return {'CANCELLED'}
prop = {'name': strip.name,
'description': '',
'properties': {'status': 'todo',
'notes': '',
'trim_start_in_frames': strip.frame_offset_start,
'duration_in_edit_in_frames': strip.frame_final_duration,
'cut_in_timeline_in_frames': strip.frame_final_start},
'order': 0,
'node_type': 'attract_shot',
'project': blender.preferences().attract_project.project,
'user': user_uuid}
# Create a Node item with the attract API
node = Node(prop)
post = pillar.sync_call(node.create)
# Populate the strip with the freshly generated ObjectID and info
if not post:
self.report({'ERROR'}, 'Error creating node! Check the console for now.')
return {'CANCELLED'}
strip.atc_object_id = node['_id']
strip.atc_is_synced = True
strip.atc_name = node['name']
strip.atc_description = node['description']
strip.atc_notes = node['properties']['notes']
strip.atc_status = node['properties']['status']
draw.tag_redraw_all_sequencer_editors()
def submit_update(self, strip):
import pillarsdk
from .. import pillar
patch = {
'op': 'from-blender',
'$set': {
'name': strip.atc_name,
'properties.trim_start_in_frames': strip.frame_offset_start,
'properties.duration_in_edit_in_frames': strip.frame_final_duration,
'properties.cut_in_timeline_in_frames': strip.frame_final_start,
'properties.status': strip.atc_status,
}
}
node = pillarsdk.Node({'_id': strip.atc_object_id})
result = pillar.sync_call(node.patch, patch)
log.info('PATCH result: %s', result)
def relink(self, strip, atc_object_id):
from .. import pillar
try:
node = pillar.sync_call(Node.find, atc_object_id)
except (sdk_exceptions.ResourceNotFound, sdk_exceptions.MethodNotAllowed):
self.report({'ERROR'}, 'Shot %r not found on the Attract server, unable to relink.'
% atc_object_id)
return {'CANCELLED'}
strip.atc_is_synced = True
strip.atc_name = node.name
strip.atc_object_id = node['_id']
# We do NOT set the position/cuts of the shot, that always has to come from Blender.
strip.atc_status = node.properties.status
strip.atc_notes = node.properties.notes or ''
strip.atc_description = node.description or ''
draw.tag_redraw_all_sequencer_editors()
class AttractShotSubmitNew(AttractOperatorMixin, Operator):
bl_idname = "attract.shot_submit_new"
bl_label = "Submit to Attract"
@classmethod
def poll(cls, context):
strip = active_strip(context)
return not strip.atc_object_id
def execute(self, context):
strip = active_strip(context)
if strip.atc_object_id:
return
node_type = self.find_node_type('attract_shot')
if isinstance(node_type, set): # in case of error
return node_type
return self.submit_new_strip(strip) or {'FINISHED'}
class AttractShotFetchUpdate(AttractOperatorMixin, Operator):
bl_idname = "attract.shot_fetch_update"
bl_label = "Fetch update from Attract"
@classmethod
def poll(cls, context):
strip = active_strip(context)
return strip is not None and getattr(strip, 'atc_object_id', None)
def execute(self, context):
strip = active_strip(context)
status = self.relink(strip, strip.atc_object_id)
if isinstance(status, set):
return status
self.report({'INFO'}, "Shot {0} refreshed".format(strip.atc_name))
return {'FINISHED'}
class AttractShotRelink(AttractShotFetchUpdate):
bl_idname = "attract.shot_relink"
bl_label = "Relink with Attract"
strip_atc_object_id = bpy.props.StringProperty()
@classmethod
def poll(cls, context):
strip = active_strip(context)
return strip is not None and not getattr(strip, 'atc_object_id', None)
def execute(self, context):
strip = active_strip(context)
status = self.relink(strip, self.strip_atc_object_id)
if isinstance(status, set):
return status
strip.atc_object_id = self.strip_atc_object_id
self.report({'INFO'}, "Shot {0} relinked".format(strip.atc_name))
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, 'strip_atc_object_id', text='Shot ID')
class AttractShotSubmitUpdate(AttractOperatorMixin, Operator):
bl_idname = 'attract.shot_submit_update'
bl_label = 'Submit update'
bl_description = 'Sends local changes to Attract'
def execute(self, context):
strip = active_strip(context)
self.submit_update(strip)
self.report({'INFO'}, 'Shot was updated on Attract')
return {'FINISHED'}
class AttractShotDelete(AttractOperatorMixin, Operator):
bl_idname = 'attract.shot_delete'
bl_label = 'Delete'
bl_description = 'Remove from Attract'
confirm = bpy.props.BoolProperty(name='confirm')
def execute(self, context):
from .. import pillar
if not self.confirm:
self.report({'WARNING'}, 'Delete aborted.')
return {'CANCELLED'}
strip = active_strip(context)
node = pillar.sync_call(Node.find, strip.atc_object_id)
if not pillar.sync_call(node.delete):
print('Unable to delete the strip node on Attract.')
return {'CANCELLED'}
remove_atc_props(strip)
draw.tag_redraw_all_sequencer_editors()
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, 'confirm', text="I hereby confirm I want to delete this shot.")
class AttractStripUnlink(AttractOperatorMixin, Operator):
bl_idname = 'attract.strip_unlink'
bl_label = 'Unlink'
bl_description = 'Remove Attract props from the selected strip(s)'
def execute(self, context):
for strip in context.selected_sequences:
atc_object_id = getattr(strip, 'atc_object_id')
remove_atc_props(strip)
if atc_object_id:
self.report({'INFO'}, 'Shot %s has been unlinked from Attract.' % atc_object_id)
draw.tag_redraw_all_sequencer_editors()
return {'FINISHED'}
class AttractShotSubmitSelected(AttractOperatorMixin, Operator):
bl_idname = 'attract.submit_selected'
bl_label = 'Submit all selected'
bl_description = 'Submits all selected strips to Attract'
@classmethod
def poll(cls, context):
return bool(context.selected_sequences)
def execute(self, context):
# Check that the project is set up for Attract.
node_type = self.find_node_type('attract_shot')
if isinstance(node_type, set):
return node_type
for strip in context.selected_sequences:
status = self.submit(strip)
if isinstance(status, set):
return status
self.report({'INFO'}, 'All selected strips sent to Attract.')
return {'FINISHED'}
def submit(self, strip):
atc_object_id = getattr(strip, 'atc_object_id', None)
# Submit as new?
if not atc_object_id:
return self.submit_new_strip(strip)
# Or just save to Attract.
return self.submit_update(strip)
def register():
bpy.types.Sequence.atc_is_synced = bpy.props.BoolProperty(name="Is synced")
bpy.types.Sequence.atc_object_id = bpy.props.StringProperty(name="Attract Object ID")
bpy.types.Sequence.atc_name = bpy.props.StringProperty(name="Shot Name")
bpy.types.Sequence.atc_description = bpy.props.StringProperty(name="Shot description")
bpy.types.Sequence.atc_notes = bpy.props.StringProperty(name="Shot notes")
# TODO: get this from the project's node type definition.
bpy.types.Sequence.atc_status = bpy.props.EnumProperty(
items=[
('on_hold', 'On hold', 'The shot is on hold'),
('todo', 'Todo', 'Waiting'),
('in_progress', 'In progress', 'The show has been assigned'),
('review', 'Review', ''),
('final', 'Final', ''),
],
name="Status")
bpy.types.Sequence.atc_order = bpy.props.IntProperty(name="Order")
bpy.utils.register_class(ToolsPanel)
bpy.utils.register_class(AttractShotSubmitNew)
bpy.utils.register_class(AttractShotRelink)
bpy.utils.register_class(AttractShotSubmitUpdate)
bpy.utils.register_class(AttractShotDelete)
bpy.utils.register_class(AttractStripUnlink)
bpy.utils.register_class(AttractShotFetchUpdate)
bpy.utils.register_class(AttractShotSubmitSelected)
draw.callback_enable()
def unregister():
draw.callback_disable()
del bpy.types.Sequence.atc_is_synced
del bpy.types.Sequence.atc_object_id
del bpy.types.Sequence.atc_name
del bpy.types.Sequence.atc_description
del bpy.types.Sequence.atc_notes
del bpy.types.Sequence.atc_status
del bpy.types.Sequence.atc_order
bpy.utils.unregister_module(__name__)

View File

@@ -0,0 +1,148 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# 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 2
# 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8-80 compliant>
import bpy
import logging
import collections
log = logging.getLogger(__name__)
strip_status_colour = {
None: (0.7, 0.7, 0.7),
'approved': (0.6392156862745098, 0.8784313725490196, 0.30196078431372547),
'final': (0.9058823529411765, 0.9607843137254902, 0.8274509803921568),
'in_progress': (1.0, 0.7450980392156863, 0.0),
'on_hold': (0.796078431372549, 0.6196078431372549, 0.08235294117647059),
'review': (0.8941176470588236, 0.9607843137254902, 0.9764705882352941),
'todo': (1.0, 0.5019607843137255, 0.5019607843137255)
}
def get_strip_rectf(strip, pixel_size_y):
# Get x and y in terms of the grid's frames and channels
x1 = strip.frame_final_start
x2 = strip.frame_final_end
y1 = strip.channel + 0.2 - pixel_size_y
y2 = y1 + 2 * pixel_size_y
return (x1, y1, x2, y2)
def draw_underline_in_strip(strip_coords, pixel_size, color):
from bgl import glColor4f, glRectf, glEnable, glDisable, GL_BLEND
context = bpy.context
# Strip coords
s_x1, s_y1, s_x2, s_y2 = strip_coords
# be careful not to draw over the current frame line
cf_x = context.scene.frame_current_final
glColor4f(*color)
glEnable(GL_BLEND)
if s_x1 < cf_x < s_x2:
# Bad luck, the line passes our strip
glRectf(s_x1, s_y1, cf_x - pixel_size, s_y2)
glRectf(cf_x + pixel_size, s_y1, s_x2, s_y2)
else:
# Normal, full rectangle draw
glRectf(s_x1, s_y1, s_x2, s_y2)
glDisable(GL_BLEND)
def draw_callback_px():
context = bpy.context
if not context.scene.sequence_editor:
return
region = context.region
xwin1, ywin1 = region.view2d.region_to_view(0, 0)
xwin2, ywin2 = region.view2d.region_to_view(region.width, region.height)
one_pixel_further_x, one_pixel_further_y = region.view2d.region_to_view(1, 1)
pixel_size_x = one_pixel_further_x - xwin1
pixel_size_y = one_pixel_further_y - ywin1
if context.scene.sequence_editor.meta_stack:
strips = context.scene.sequence_editor.meta_stack[-1].sequences
else:
strips = context.scene.sequence_editor.sequences
for strip in strips:
if not strip.atc_object_id:
continue
# Get corners (x1, y1), (x2, y2) of the strip rectangle in px region coords
strip_coords = get_strip_rectf(strip, pixel_size_y)
# check if any of the coordinates are out of bounds
if strip_coords[0] > xwin2 or strip_coords[2] < xwin1 or strip_coords[1] > ywin2 or \
strip_coords[3] < ywin1:
continue
# Draw
status = strip.atc_status
if status in strip_status_colour:
color = strip_status_colour[status]
else:
color = strip_status_colour[None]
alpha = 1.0 if strip.atc_is_synced else 0.5
draw_underline_in_strip(strip_coords, pixel_size_x, color + (alpha, ))
def tag_redraw_all_sequencer_editors():
context = bpy.context
# Py cant access notifiers
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'SEQUENCE_EDITOR':
for region in area.regions:
if region.type == 'WINDOW':
region.tag_redraw()
# This is a list so it can be changed instead of set
# if it is only changed, it does not have to be declared as a global everywhere
cb_handle = []
def callback_enable():
if cb_handle:
return
cb_handle[:] = bpy.types.SpaceSequenceEditor.draw_handler_add(
draw_callback_px, (), 'WINDOW', 'POST_VIEW'),
tag_redraw_all_sequencer_editors()
def callback_disable():
if not cb_handle:
return
bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], 'WINDOW')
tag_redraw_all_sequencer_editors()

View File

@@ -29,7 +29,7 @@ from bpy.types import AddonPreferences, Operator, WindowManager, Scene, Property
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty
import rna_prop_ui
from . import pillar
from . import pillar, async_loop
PILLAR_SERVER_URL = 'https://cloud.blender.org/api/'
# PILLAR_SERVER_URL = 'http://pillar:5001/api/'
@@ -45,6 +45,8 @@ def redraw(self, context):
def blender_syncable_versions(self, context):
"""Returns the list of items used by SyncStatusProperties.version EnumProperty."""
bss = context.window_manager.blender_sync_status
versions = bss.available_blender_versions
if not versions:
@@ -104,6 +106,42 @@ class SyncStatusProperties(PropertyGroup):
self['available_blender_versions'] = new_versions
def bcloud_available_projects(self, context):
"""Returns the list of items used by BlenderCloudProjectGroup.project EnumProperty."""
attr_proj = preferences().attract_project
projs = attr_proj.available_projects
if not projs:
return [('', 'No projects available in your Blender Cloud', '')]
return [(p['_id'], p['name'], '') for p in projs]
class BlenderCloudProjectGroup(PropertyGroup):
status = EnumProperty(
items=[
('NONE', 'NONE', 'We have done nothing at all yet'),
('IDLE', 'IDLE', 'User requested something, which is done, and we are now idle'),
('FETCHING', 'FETCHING', 'Fetching available projects from Blender Cloud'),
],
name='status',
update=redraw)
project = EnumProperty(
items=bcloud_available_projects,
name='Cloud project',
description='Which Blender Cloud project to work with')
# List of projects is stored in 'available_projects' ID property,
# because I don't know how to store a variable list of strings in a proper RNA property.
@property
def available_projects(self) -> list:
return self.get('available_projects', [])
@available_projects.setter
def available_projects(self, new_projects):
self['available_projects'] = new_projects
class BlenderCloudPreferences(AddonPreferences):
bl_idname = ADDON_NAME
@@ -127,6 +165,8 @@ class BlenderCloudPreferences(AddonPreferences):
default=True
)
attract_project = PointerProperty(type=BlenderCloudProjectGroup)
def draw(self, context):
import textwrap
@@ -205,9 +245,16 @@ class BlenderCloudPreferences(AddonPreferences):
# Image Share stuff
share_box = layout.box()
share_box.label('Image Sharing on Blender Cloud', icon_value=icon('CLOUD'))
texture_box.enabled = msg_icon != 'ERROR'
share_box.enabled = msg_icon != 'ERROR'
share_box.prop(self, 'open_browser_after_share')
# Attract stuff
attract_box = layout.box()
attract_box.enabled = msg_icon != 'ERROR'
attract_row = attract_box.row(align=True)
attract_row.label('Attract', icon_value=icon('CLOUD'))
self.draw_attract_buttons(attract_row, self.attract_project)
def draw_subscribe_button(self, layout):
layout.operator('pillar.subscribe', icon='WORLD')
@@ -242,12 +289,32 @@ class BlenderCloudPreferences(AddonPreferences):
else:
row_pull.label('Cloud Sync is running.')
def draw_attract_buttons(self, layout, bcp: BlenderCloudProjectGroup):
layout.enabled = bcp.status in {'NONE', 'IDLE'}
row_buttons = layout.row(align=True)
projects = bcp.available_projects
project = bcp.project
if bcp.status in {'NONE', 'IDLE'}:
if not projects or not project:
row_buttons.operator('pillar.projects',
text='Find project to load',
icon='FILE_REFRESH')
else:
row_buttons.prop(bcp, 'project')
row_buttons.operator('pillar.projects',
text='',
icon='FILE_REFRESH')
else:
row_buttons.label('Fetching available projects.')
class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
Operator):
"""Updates the Pillar URL and tests the new URL."""
bl_idname = 'pillar.credentials_update'
bl_label = 'Update credentials'
bl_description = 'Resynchronises your Blender ID login with Blender Cloud'
log = logging.getLogger('bpy.ops.%s' % bl_idname)
@@ -304,6 +371,67 @@ class PILLAR_OT_subscribe(Operator):
return {'FINISHED'}
class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
pillar.PillarOperatorMixin,
Operator):
"""Fetches the projects available to the user, and ."""
bl_idname = 'pillar.projects'
bl_label = 'Fetch available projects'
stop_upon_exception = True
_log = logging.getLogger('bpy.ops.%s' % bl_idname)
async def async_execute(self, context):
import pillarsdk
from .pillar import pillar_call
self._log.info('Checking credentials')
try:
db_user = await self.check_credentials(context, ())
except pillar.UserNotLoggedInError as ex:
self._log.info('Not logged in error raised: %s', ex)
self.report({'ERROR'}, 'Please log in on Blender ID first.')
self.quit()
return
user_id = db_user['_id']
self.log.info('Going to fetch projects for user %s', user_id)
preferences().attract_project.status = 'FETCHING'
# Get all projects, except the home project.
projects_user = await pillar_call(
pillarsdk.Project.all,
{'where': {'user': user_id,
'category': {'$ne': 'home'}},
'sort': '-_created',
'projection': {'_id': True,
'name': True},
})
projects_shared = await pillar_call(
pillarsdk.Project.all,
{'where': {'user': {'$ne': user_id},
'permissions.groups.group': {'$in': db_user.groups}},
'sort': '-_created',
'projection': {'_id': True,
'name': True},
})
# We need to convert to regular dicts before storing in ID properties.
# Also don't store more properties than we need.
projects = [{'_id': p['_id'], 'name': p['name']} for p in projects_user['_items']] + \
[{'_id': p['_id'], 'name': p['name']} for p in projects_shared['_items']]
preferences().attract_project.available_projects = projects
self.quit()
def quit(self):
preferences().attract_project.status = 'IDLE'
super().quit()
class PILLAR_PT_image_custom_properties(rna_prop_ui.PropertyPanel, bpy.types.Panel):
"""Shows custom properties in the image editor."""
@@ -353,10 +481,12 @@ def icon(icon_name: str) -> int:
def register():
bpy.utils.register_class(BlenderCloudProjectGroup)
bpy.utils.register_class(BlenderCloudPreferences)
bpy.utils.register_class(PillarCredentialsUpdate)
bpy.utils.register_class(SyncStatusProperties)
bpy.utils.register_class(PILLAR_OT_subscribe)
bpy.utils.register_class(PILLAR_OT_projects)
bpy.utils.register_class(PILLAR_PT_image_custom_properties)
addon_prefs = preferences()
@@ -385,10 +515,12 @@ def register():
def unregister():
unload_custom_icons()
bpy.utils.unregister_class(BlenderCloudProjectGroup)
bpy.utils.unregister_class(PillarCredentialsUpdate)
bpy.utils.unregister_class(BlenderCloudPreferences)
bpy.utils.unregister_class(SyncStatusProperties)
bpy.utils.unregister_class(PILLAR_OT_subscribe)
bpy.utils.unregister_class(PILLAR_OT_projects)
bpy.utils.unregister_class(PILLAR_PT_image_custom_properties)
del WindowManager.last_blender_cloud_location

View File

@@ -146,6 +146,13 @@ def blender_id_subclient() -> dict:
return subclient
def pillar_user_uuid() -> str:
"""Returns the UUID of the Pillar user."""
import blender_id
return blender_id.get_subclient_user_id(SUBCLIENT_ID)
def pillar_api(pillar_endpoint: str = None, caching=True) -> pillarsdk.Api:
"""Returns the Pillar SDK API object for the current user.
@@ -198,6 +205,12 @@ pillar_semaphore = asyncio.Semaphore(3)
async def pillar_call(pillar_func, *args, caching=True, **kwargs):
"""Calls a Pillar function.
A semaphore is used to ensure that there won't be too many
calls to Pillar simultaneously.
"""
partial = functools.partial(pillar_func, *args, api=pillar_api(caching=caching), **kwargs)
loop = asyncio.get_event_loop()
@@ -205,6 +218,12 @@ async def pillar_call(pillar_func, *args, caching=True, **kwargs):
return await loop.run_in_executor(None, partial)
def sync_call(pillar_func, *args, **kwargs):
"""Synchronous call to Pillar, ensures the correct Api object is used."""
return pillar_func(*args, api=pillar_api(), **kwargs)
async def check_pillar_credentials(required_roles: set):
"""Tries to obtain the user at Pillar using the user's credentials.

View File

@@ -1,7 +1,7 @@
# Primary requirements:
-e git+https://github.com/sybrenstuvel/cachecontrol.git@sybren-filecache-delete-crash-fix#egg=CacheControl
lockfile==0.12.2
pillarsdk==1.5.0
pillarsdk==1.6.0
wheel==0.29.0
# Secondary requirements:

View File

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