diff --git a/operators/sequencer.py b/operators/sequencer.py index 803960d..99005b8 100644 --- a/operators/sequencer.py +++ b/operators/sequencer.py @@ -9,7 +9,7 @@ from bpy.props import (BoolProperty, StringProperty, FloatProperty, from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels, get_channel_index, new_text_strip, get_strip_at, get_channel_name, - create_shot_strip) + create_shot_strip, update_text_strips) from vse_toolbox.scene_cut_detection import detect_scene_change from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings, get_addon_prefs @@ -24,7 +24,11 @@ class VSETB_OT_rename(Operator): #template : StringProperty(name="Strip Name", default="") #increment : IntProperty(name="Increment", default=0) - selected_only : BoolProperty(name="Selected Only", default=True) + selected_only : BoolProperty( + name="Selected Only", + default=False, + description="Rename only selected strips; disabled renames the whole channel", + ) #start_number : IntProperty(name="Start Number", default=0, min=0) #by_sequence : BoolProperty( # name="Reset By Sequence", @@ -34,9 +38,13 @@ class VSETB_OT_rename(Operator): @classmethod def poll(cls, context): - settings = get_scene_settings() strip = context.active_strip - return settings.active_project and get_channel_name(strip) in ('Shots', 'Sequences') + if strip is None and context.scene.sequence_editor: + strip = next( + (s for s in context.scene.sequence_editor.strips if s.select), + None, + ) + return strip is not None def invoke(self, context, event): scn = context.scene @@ -49,19 +57,29 @@ class VSETB_OT_rename(Operator): scn = context.scene settings = get_scene_settings() project = settings.active_project + if project is None and settings.projects: + project = settings.projects[0] + if project is None: + self.report({'ERROR'}, 'No VSE Toolbox project is loaded') + return episode = project.episode_name sequence = str(project.sequence_start_number).zfill(project.sequence_padding) shot = str(project.shot_start_number).zfill(project.shot_padding) strip = context.active_strip + if strip is None and context.scene.sequence_editor: + strip = next( + (s for s in context.scene.sequence_editor.strips if s.select), + None, + ) channel_name = get_channel_name(strip) col = layout.column() col.use_property_split = True col.use_property_decorate = False - if channel_name == 'Shots': + if channel_name != 'Sequences': col.prop(project, 'shot_template', text='Shot Name') col.prop(project, 'shot_start_number', text='Start Number') col.prop(project, 'shot_increment', text='Increment') @@ -77,9 +95,9 @@ class VSETB_OT_rename(Operator): col.prop(self, 'selected_only') - if channel_name == 'Shots': + if channel_name != 'Sequences': label = project.shot_template.format(episode=episode, sequence=sequence, shot=shot) - elif channel_name == 'Sequences': + else: label = project.sequence_template.format(episode=episode, sequence=sequence) col.label(text=f'Renaming {label}') @@ -88,12 +106,30 @@ class VSETB_OT_rename(Operator): scn = context.scene settings = get_scene_settings() project = settings.active_project + if project is None: + # The config path is set during addon registration, but the + # project data is only populated when Load Settings is pressed. + bpy.ops.vse_toolbox.load_settings() + project = settings.active_project + if project is None and settings.projects: + project = settings.projects[0] + if project is None: + self.report({'ERROR'}, 'No VSE Toolbox project is loaded') + return {'CANCELLED'} + # Keep the scene's active project in sync with the fallback project; + # rename_strips reads it again from scene settings. + settings.project_name = project.name strip = context.active_strip + if strip is None and context.scene.sequence_editor: + strip = next( + (s for s in context.scene.sequence_editor.strips if s.select), + None, + ) channel_name = get_channel_name(strip) strips = get_strips(channel=channel_name, selected_only=self.selected_only) - if channel_name == 'Shots': + if channel_name != 'Sequences': rename_strips(strips, template=project.shot_template, increment=project.shot_increment, start_number=project.shot_start_number, @@ -152,8 +188,13 @@ class VSETB_OT_set_sequencer(Operator): movie = movies[0] movie.transform.scale_x = movie.transform.scale_y = 1 elem = movie.strip_elem_from_frame(scn.frame_current) - scn.render.resolution_x = elem.orig_width - scn.render.resolution_y = elem.orig_height + if elem is None and movie.elements: + elem = movie.elements[0] + if elem is not None: + scn.render.resolution_x = elem.orig_width + scn.render.resolution_y = elem.orig_height + else: + self.report({'WARNING'}, 'Cannot determine movie resolution.') else: self.report({'INFO'}, f'Cannot set Resolution. No Movie Found.') @@ -200,16 +241,32 @@ class VSETB_OT_scene_cut_detection(Operator): movie_type: EnumProperty( items=[('ANIMATED', 'Animated', 'Use select filter from ffmpeg, best for animated frame'), ('STILL', 'Still', 'Use freezedetect filter from ffmpeg, best for board or text')], - name='Movie Type') + name='Movie Type', default='ANIMATED') def invoke(self, context, event): - if context.scene.sequence_editor.channels.get('Shots'): - self.destination_channel_name = 'Shots' + sequencer = context.scene.sequence_editor + channels = sequencer.channels if sequencer else None + selected_strip = context.active_strip + if selected_strip is None and context.selected_strips: + selected_strip = context.selected_strips[0] - # Select active channel by default - if (strip := context.active_strip) and get_channel_name(strip) != 'Shots': - self.source_channel_name = get_channel_name(strip) + # Prefer the channel containing the selected strip. With no selected + # strip, use the first available channel (channel 0 in the UI list). + if selected_strip is not None: + source_channel = selected_strip.channel + self.source_channel_name = get_channel_name(selected_strip) + elif channels and len(channels): + source_channel = 0 + self.source_channel_name = channels[0].name + else: + source_channel = 0 + + # Put the result on the immediately following channel when it exists. + # Do not overwrite it when that channel is unavailable. + if channels and source_channel + 1 < len(channels): + destination_channel = channels[source_channel + 1] + self.destination_channel_name = destination_channel.name self.frame_start = context.scene.frame_start self.frame_end = context.scene.frame_end @@ -251,6 +308,12 @@ class VSETB_OT_scene_cut_detection(Operator): def modal(self, context, event): scn = context.scene + # Replace the previous detection result instead of mixing it with the + # new one. This is especially important when changing thresholds. + for old_strip in list(scn.sequence_editor.strips): + if old_strip.name.startswith("tmp_shot_"): + scn.sequence_editor.strips.remove(old_strip) + strips = get_strips(channel=self.source_channel_name) i = 1 @@ -385,7 +448,9 @@ class VSETB_OT_set_stamps(Operator): #stamps_strip = scn.sequence_editor.sequences.new_meta('Stamps', scn.frame_start, scn.frame_end) #stamps_strip.channel = get_channel_index('Stamps') - scn.frame_set(scn.frame_current) # For update stamps + # frame_set() does not trigger frame_change_post when the frame does + # not change, so update the stamp text explicitly for the first view. + update_text_strips(scn) return {"FINISHED"} @@ -748,7 +813,7 @@ class VSETB_OT_create_sequence_strip(Operator): sequence_name: StringProperty( name="Sequence Name", description="Name of the sequence (e.g. SC010)", - default="SC010", + default="SQ010", ) @classmethod @@ -763,6 +828,37 @@ class VSETB_OT_create_sequence_strip(Operator): return True def invoke(self, context, event): + selected_shots = sorted( + (s for s in context.selected_strips if get_channel_name(s) == 'Shots'), + key=lambda s: s.frame_final_start, + ) + if selected_shots: + first_frame = selected_shots[0].frame_final_start + sequence_channel = get_channel_index('Sequences') + previous = None + if sequence_channel: + previous = max( + ( + s for s in context.scene.sequence_editor.strips + if s.channel == sequence_channel + and s.frame_final_end <= first_frame + ), + key=lambda s: s.frame_final_end, + default=None, + ) + + if previous: + match = re.search(r'(\d+)$', previous.name) + if match: + settings = get_scene_settings() + project = settings.active_project + increment = project.sequence_increment if project else 10 + number = int(match.group(1)) + increment + self.sequence_name = ( + previous.name[:match.start()] + + str(number).zfill(len(match.group(1))) + ) + return context.window_manager.invoke_props_dialog(self) def execute(self, context): diff --git a/operators/tracker.py b/operators/tracker.py index be3e6a6..0865837 100644 --- a/operators/tracker.py +++ b/operators/tracker.py @@ -205,6 +205,7 @@ class VSETB_OT_load_projects(Operator): #print(metadata_data) task_status = project.task_statuses.add() task_status.name = status_data['short_name'].upper() + task_status.is_done = status_data.get('is_done', False) project.task_types.clear() for task_type_data in tracker.get_shot_task_types(project_data): @@ -228,7 +229,10 @@ class VSETB_OT_load_projects(Operator): if project.name not in project_names: settings.projects.remove(list(settings.projects).index(project)) - if self.ctrl or not settings.get('projects_loaded'): + # ``load_projects`` can also be called programmatically (for example + # from the upload dialog), bypassing ``invoke`` and therefore not + # initializing ``self.ctrl``. + if getattr(self, 'ctrl', False) or not settings.get('projects_loaded'): bpy.ops.vse_toolbox.load_settings() if prev_project_name != '/' and prev_project_name in settings.projects: @@ -293,11 +297,19 @@ class VSETB_OT_new_episode(Operator): class VSETB_OT_upload_to_tracker(Operator): bl_idname = "vse_toolbox.upload_to_tracker" bl_label = "Upload to tracker" - bl_description = "Upload selected strip to tracker" + bl_description = ( + "Upload the selected strips on the Shots track to the tracker. " + "Creates missing sequences, shots and tasks, posts a comment " + '(with a preview, rendered if "Render Strips" is enabled) and ' + "updates frames, metadata, casting and task comments." + ) bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): + if not get_strips(channel='Shots', selected_only=True): + cls.poll_message_set('Select at least one shot strip') + return False return True def invoke(self, context, event): @@ -307,7 +319,14 @@ class VSETB_OT_upload_to_tracker(Operator): tracker = prefs.tracker tracker.connect() - + + # Loading the toolbox config can create the selected project locally, + # but does not populate its tracker metadata. In that case the task + # enum has no items even though Kitsu has shot task types configured. + if self.project and not self.project.task_types: + bpy.ops.vse_toolbox.load_projects() + self.project = settings.active_project + #self.bl_label = f"Upload to {settings.tracker_name.title()}" return context.window_manager.invoke_props_dialog(self, width=350) @@ -363,10 +382,26 @@ class VSETB_OT_upload_to_tracker(Operator): format_data = {**settings.format_data, **self.project.format_data} status = upload_to_tracker.status - if status == 'CURRENT': + keep_current_status = status == 'CURRENT' + if keep_current_status: status = None - + + # ``CURRENT`` means preserve an existing task's status. A newly + # created task has no current status, so use Kitsu's done metadata for + # the first upload (prefer DONE when several statuses are marked done). + default_new_task_status = None + if keep_current_status: + done_statuses = [s for s in self.project.task_statuses if s.is_done] + default_new_task_status = next( + (s.name for s in done_statuses if s.name.lower() == 'done'), + done_statuses[0].name if done_statuses else None, + ) + shot_strips = get_strips(channel='Shots', selected_only=True) + if not shot_strips: + self.report({'WARNING'}, 'Select at least one shot strip to upload') + return {'CANCELLED'} + context.window_manager.progress_begin(0, len(shot_strips)) for i, strip in enumerate(shot_strips): @@ -392,7 +427,11 @@ class VSETB_OT_upload_to_tracker(Operator): task = tracker.get_task(upload_to_tracker.task, entity=shot) if not task: - task = tracker.new_task(shot, task_type=upload_to_tracker.task) + task = tracker.new_task( + shot, + task_type=upload_to_tracker.task, + status=default_new_task_status or status, + ) preview = None if upload_to_tracker.add_preview: @@ -439,9 +478,16 @@ class VSETB_OT_upload_to_tracker(Operator): if upload_to_tracker.custom_data: params['custom_data'] = metadata params['description'] = strip_settings.description - + if upload_to_tracker.update_frames: + # Blender's frame_final_end is exclusive, while Kitsu's + # frame_out is inclusive. Keep both the explicit range and + # the duration in sync with the uploaded strip. params['frames'] = strip.frame_final_duration + params.setdefault('custom_data', {}).update({ + 'frame_in': strip.frame_final_start, + 'frame_out': strip.frame_final_end - 1, + }) if params: tracker.update_data(shot, **params) diff --git a/resources/trackers/kitsu.py b/resources/trackers/kitsu.py index 48b5287..e64f4bf 100644 --- a/resources/trackers/kitsu.py +++ b/resources/trackers/kitsu.py @@ -120,6 +120,11 @@ class Kitsu(Tracker): return os.environ['TRACKER_PROJECT_NAME'] def get_projects(self): + # gazu caches this endpoint indefinitely by default. This matters for + # projects created or configured after Blender started: the project + # may be present locally (from the toolbox config) while its task + # types are never loaded from Kitsu. + gazu.cache.clear_all() return gazu.project.all_open_projects() def get_episode(self, episode, project=None): diff --git a/sequencer_utils.py b/sequencer_utils.py index ae60b46..64da267 100644 --- a/sequencer_utils.py +++ b/sequencer_utils.py @@ -103,11 +103,27 @@ def get_channel_name(strip): return scn.sequence_editor.channels[strip.channel].name def get_strip_sequence_name(strip): - sequence_strip = get_strip_at(channel='Sequences', frame=strip.frame_final_start) + """Return the sequence strip directly above a shot strip.""" + scn = bpy.context.scene + sequencer = scn.sequence_editor + if not sequencer or not strip: + return 'NoSequence' + + # In the GAV layout, sequence strips are placed immediately above shot + # strips (for example, Sequences on channel 3 and Shots on channel 2). + sequence_strip = next( + ( + candidate for candidate in sequencer.strips + if candidate.channel == strip.channel + 1 + and candidate.frame_final_start <= strip.frame_final_start + and candidate.frame_final_end >= strip.frame_final_end + ), + None, + ) if sequence_strip: return sequence_strip.name - else: - return 'NoSequence' + + return 'NoSequence' def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_sequence=False): scn = bpy.context.scene @@ -121,12 +137,24 @@ def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_ prev_sequence_name = None strip_number = 0 - for strip in strips: + planned_names = [] + for strip in strips: channel = get_channel_name(strip) sequence_name = get_strip_sequence_name(strip) format_data = {} if channel == 'Shots': - format_data = parse(project.sequence_template, sequence_name) + format_data = ( + parse(project.sequence_template, sequence_name) + or parse(project.sequence_template, sequence_name.lower()) + or {} + ) + if 'sequence' not in format_data: + # Shots created directly from an animatic do not have a + # parent Sequences strip yet. Use the configured first + # sequence number until one is created. + format_data['sequence'] = str( + project.sequence_start_number + ).zfill(project.sequence_padding) else: format_data['sequence'] = str(strip_number*increment + start_number).zfill(padding) @@ -141,15 +169,18 @@ def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_ name = template.format(**format_data) - existing_strip = scn.sequence_editor.strips_all.get(name) - if existing_strip: - existing_strip.name = f"{name}_tmp" - + planned_names.append((strip, name)) + prev_sequence_name = sequence_name + strip_number += 1 + + # Clear all names first so renaming never collides with a later strip and + # leaves a permanent `_tmp` suffix behind. + for index, (strip, _) in enumerate(planned_names): + strip.name = f"__vse_rename_{index:04d}" + + for strip, name in planned_names: print(f'Renaming {strip.name} -> {name}') strip.name = name - - prev_sequence_name = sequence_name - strip_number += 1 def set_channels(): scn = bpy.context.scene diff --git a/ui/panels.py b/ui/panels.py index ffc3601..c3c559a 100644 --- a/ui/panels.py +++ b/ui/panels.py @@ -6,9 +6,9 @@ import bpy from bpy.types import Panel, Menu from bl_ui.utils import PresetPanel -from vse_toolbox.bl_utils import (get_addon_prefs, get_scene_settings, get_strip_settings) +from vse_toolbox.bl_utils import get_addon_prefs, get_scene_settings, get_strip_settings from vse_toolbox.constants import ASSET_PREVIEWS, REVIEW_TEMPLATE_BLEND -from vse_toolbox.sequencer_utils import (set_active_strip, get_channel_name, get_strips) +from vse_toolbox.sequencer_utils import set_active_strip, get_channel_name, get_strips from vse_toolbox.file_utils import norm_str @@ -30,7 +30,7 @@ class VSETB_PT_main(VSETB_main, Panel): # row.label('VSE Toolbox') # row.prop(settings, 'project_name', text='') - + # project = settings.active_project # if project and project.type == 'TVSHOW': @@ -47,21 +47,23 @@ class VSETB_PT_main(VSETB_main, Panel): row = layout.row(align=True) - row.operator('vse_toolbox.load_projects', icon='FILE_REFRESH', text='', emboss=False) + row.operator( + "vse_toolbox.load_projects", icon="FILE_REFRESH", text="", emboss=False + ) row.separator(factor=0.5) - row.prop(settings, 'project_name', text='') - + row.prop(settings, "project_name", text="") + project = settings.active_project if not project: return - if project and project.type == 'TVSHOW': + if project and project.type == "TVSHOW": row.separator(factor=0.5) - row.prop(project, 'episode_name', text='') - + row.prop(project, "episode_name", text="") + row.separator(factor=0.5) - row.prop(project, "show_settings", icon="PREFERENCES", text='') + row.prop(project, "show_settings", icon="PREFERENCES", text="") if project.show_settings: box = layout.box() split = box.split(factor=0.3) @@ -76,7 +78,7 @@ class VSETB_PT_main(VSETB_main, Panel): row.separator(factor=0.5) row.label(text="Template") - row.operator("vse_toolbox.add_template", text="", icon='ADD', emboss=False) + row.operator("vse_toolbox.add_template", text="", icon="ADD", emboss=False) for i, template in enumerate(project.templates): row = name_col.row() @@ -86,12 +88,13 @@ class VSETB_PT_main(VSETB_main, Panel): subrow = row.row() subrow.prop(template, "value", text="") row.separator(factor=0.25) - row.operator("vse_toolbox.remove_template", text="", icon='REMOVE', emboss=False).index = i - + row.operator( + "vse_toolbox.remove_template", text="", icon="REMOVE", emboss=False + ).index = i # settings = get_scene_settings() # prefs = get_addon_prefs() - + # project = settings.active_project # layout = self.layout @@ -103,13 +106,13 @@ class VSETB_PT_main(VSETB_main, Panel): # if project.type == 'TVSHOW': # col.prop(project, 'episode_name', text='Episodes') - #col.separator() + # col.separator() - #row = col.row(align=True) - - #row.prop(settings, 'toogle_prefs', text='', icon='PREFERENCES', toggle=True) - - ''' + # row = col.row(align=True) + + # row.prop(settings, 'toogle_prefs', text='', icon='PREFERENCES', toggle=True) + + """ if settings.toogle_prefs: box = col.box() col = box.column(align=True) @@ -126,21 +129,22 @@ class VSETB_PT_main(VSETB_main, Panel): #col.prop(project, 'shot_template') # col.separator() # col.operator('vse_toolbox.new_episode', text='Add Episode', icon='IMPORT') - ''' + """ # Rename + class VSETB_PT_strip(Panel): bl_space_type = "SEQUENCE_EDITOR" bl_region_type = "UI" bl_category = "Strip" bl_label = "VSE ToolBox" - #bl_order = 0 + # bl_order = 0 @classmethod def poll(cls, context): strip = context.active_strip - return strip and get_channel_name(strip) == 'Shots' + return strip and get_channel_name(strip) == "Shots" def draw(self, context): prefs = get_addon_prefs() @@ -150,22 +154,28 @@ class VSETB_PT_strip(Panel): layout.use_property_split = True layout.use_property_decorate = False - layout.prop(settings, 'source_name', text='Source Name') - + layout.prop(settings, "source_name", text="Source Name") + class VSETB_PT_sequencer(VSETB_main, Panel): bl_label = "Sequencer" - #bl_parent_id = "VSETB_PT_main" - + # bl_parent_id = "VSETB_PT_main" + def draw_header_preset(self, context): settings = get_scene_settings() - audio_strips = [s for s in get_strips('Audio') if s.type == "SOUND"] + audio_strips = [s for s in get_strips("Audio") if s.type == "SOUND"] depress = any(s.show_waveform for s in audio_strips) - self.layout.operator('vse_toolbox.show_waveform', text="", icon="IPO_ELASTIC", depress=depress).enabled = not depress + self.layout.operator( + "vse_toolbox.show_waveform", text="", icon="IPO_ELASTIC", depress=depress + ).enabled = not depress - ico = ("RESTRICT_SELECT_OFF" if settings.auto_select_strip else "RESTRICT_SELECT_ON") + ico = ( + "RESTRICT_SELECT_OFF" + if settings.auto_select_strip + else "RESTRICT_SELECT_ON" + ) self.layout.prop(settings, "auto_select_strip", text="", icon=ico) def draw(self, context): @@ -175,21 +185,35 @@ class VSETB_PT_sequencer(VSETB_main, Panel): project = settings.active_project strip = context.active_strip - channel = get_channel_name(strip) + if strip is None and context.scene.sequence_editor: + strip = next( + (s for s in context.scene.sequence_editor.strips if s.select), + None, + ) + channel = get_channel_name(strip) or "Selected Channel" col = layout.column() - col.operator('vse_toolbox.set_sequencer', text='Set-Up Sequencer', icon='SEQ_SEQUENCER') - col.operator('vse_toolbox.strips_rename', text=f'Rename {channel}', icon='SORTALPHA') - col.operator('vse_toolbox.set_stamps', text='Set Stamps', icon='COLOR') - col.operator("vse_toolbox.collect_files", text='Collect Files', icon='PACKAGE') + col.operator( + "vse_toolbox.strips_rename", text=f"Rename {channel}", icon="SORTALPHA" + ) + col.operator("vse_toolbox.set_stamps", text="Set Stamps", icon="COLOR") + col.operator("vse_toolbox.collect_files", text="Collect Files", icon="PACKAGE") + col.operator( + "vse_toolbox.scene_cut_detection", + text="Scene Cut Detection", + icon="SCULPTMODE_HLT", + ) + col.operator( + "vse_toolbox.create_sequence_strip", text="Create Sequence", icon="SEQUENCE" + ) class VSETB_PT_settings(VSETB_main, Panel): bl_label = "Settings" - #bl_parent_id = "VSETB_PT_main" - bl_options = {'DEFAULT_CLOSED'} - - #def draw_header_preset(self, context): + # bl_parent_id = "VSETB_PT_main" + bl_options = {"DEFAULT_CLOSED"} + + # def draw_header_preset(self, context): # self.layout.operator('vse_toolbox.import_files', icon='IMPORT', text='', emboss=False) def draw(self, context): @@ -199,19 +223,21 @@ class VSETB_PT_settings(VSETB_main, Panel): project = settings.active_project col = layout.column() - #row = col.row(align=True) - col.prop(project, 'sequence_template') - col.prop(project, 'shot_template') - col.prop(project, 'render_template') + # row = col.row(align=True) + col.prop(project, "sequence_template") + col.prop(project, "shot_template") + col.prop(project, "render_template") class VSETB_PT_imports(VSETB_main, Panel): bl_label = "Imports" - #bl_parent_id = "VSETB_PT_main" - #bl_options = {'DEFAULT_CLOSED'} - + # bl_parent_id = "VSETB_PT_main" + # bl_options = {'DEFAULT_CLOSED'} + def draw_header_preset(self, context): - self.layout.operator('vse_toolbox.import_files', icon='IMPORT', text='', emboss=False) + self.layout.operator( + "vse_toolbox.import_files", icon="IMPORT", text="", emboss=False + ) def draw(self, context): prefs = get_addon_prefs() @@ -220,27 +246,35 @@ class VSETB_PT_imports(VSETB_main, Panel): project = settings.active_project col = layout.column() - #row = col.row(align=True) - #col.operator('vse_toolbox.import_files', text='Import', icon='IMPORT') - col.operator('vse_toolbox.import_spreadsheet', text='Import Spreadsheet', icon='SPREADSHEET') - col.operator("vse_toolbox.import_shots", text='Import Shots', icon="FILE_MOVIE") - col.operator("vse_toolbox.import_stb_xml", text='Import STB XML', icon="FILE_TEXT") + # row = col.row(align=True) + # col.operator('vse_toolbox.import_files', text='Import', icon='IMPORT') + col.operator( + "vse_toolbox.import_spreadsheet", + text="Import Spreadsheet", + icon="SPREADSHEET", + ) + col.operator("vse_toolbox.import_shots", text="Import Shots", icon="FILE_MOVIE") + col.operator( + "vse_toolbox.import_stb_xml", text="Import STB XML", icon="FILE_TEXT" + ) class VSETB_PT_presets(PresetPanel, Panel): - bl_label = 'Spreadsheet Presets' - preset_subdir = 'vse_toolbox' - preset_operator = 'script.execute_preset' + bl_label = "Spreadsheet Presets" + preset_subdir = "vse_toolbox" + preset_operator = "script.execute_preset" preset_add_operator = "vse_toolbox.add_spreadsheet_preset" class VSETB_PT_exports(VSETB_main, Panel): bl_label = "Exports" - #bl_parent_id = "VSETB_PT_main" - bl_options = {'DEFAULT_CLOSED'} - + # bl_parent_id = "VSETB_PT_main" + bl_options = {"DEFAULT_CLOSED"} + def draw_header_preset(self, context): - self.layout.operator('vse_toolbox.export_spreadsheet', icon='EXPORT', text='', emboss=False) + self.layout.operator( + "vse_toolbox.export_spreadsheet", icon="EXPORT", text="", emboss=False + ) def draw(self, context): prefs = get_addon_prefs() @@ -250,18 +284,26 @@ class VSETB_PT_exports(VSETB_main, Panel): col = layout.column(align=False) # TODO FAIRE DES VRAIS OPS - col.operator('vse_toolbox.strips_render', text='Render Strips', icon='SEQUENCE') + col.operator("vse_toolbox.strips_render", text="Render Strips", icon="SEQUENCE") - tracker_label = settings.tracker_name.title().replace('_', ' ') - col.operator('vse_toolbox.upload_to_tracker', text=f'Upload to {tracker_label}', icon='EXPORT') - col.operator('vse_toolbox.export_spreadsheet', text='Export Spreadsheet', icon='SPREADSHEET') - col.operator('vse_toolbox.export_edl', text='Export edl', icon='SEQ_SEQUENCER') + tracker_label = settings.tracker_name.title().replace("_", " ") + col.operator( + "vse_toolbox.upload_to_tracker", + text=f"Upload to {tracker_label}", + icon="EXPORT", + ) + col.operator( + "vse_toolbox.export_spreadsheet", + text="Export Spreadsheet", + icon="SPREADSHEET", + ) + col.operator("vse_toolbox.export_edl", text="Export edl", icon="SEQ_SEQUENCER") class VSETB_PT_tracker(VSETB_main, Panel): bl_label = "Tracker" - #bl_parent_id = "VSETB_PT_main" - bl_options = {'DEFAULT_CLOSED'} + # bl_parent_id = "VSETB_PT_main" + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -274,19 +316,20 @@ class VSETB_PT_tracker(VSETB_main, Panel): def draw(self, context): return + class VSETB_PT_casting(VSETB_main, Panel): bl_label = "Casting" bl_parent_id = "VSETB_PT_tracker" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): strip = context.active_strip - return strip and get_channel_name(strip) == 'Shots' + return strip and get_channel_name(strip) == "Shots" def draw(self, context): layout = self.layout - + settings = get_scene_settings() strip_settings = get_strip_settings() @@ -297,35 +340,47 @@ class VSETB_PT_casting(VSETB_main, Panel): if not project.assets: row = layout.row(align=True) - row.label(text='No Assets in this Project') + row.label(text="No Assets in this Project") else: row = layout.row() col = row.column() - col.template_list("VSETB_UL_casting", "shot_casting", strip_settings, "casting", strip_settings, "casting_index", rows=6) - + col.template_list( + "VSETB_UL_casting", + "shot_casting", + strip_settings, + "casting", + strip_settings, + "casting_index", + rows=6, + ) + col_tool = row.column(align=True) - col_tool.operator('vse_toolbox.casting_add', icon='ADD', text="") - col_tool.operator('vse_toolbox.casting_remove', icon='REMOVE', text="") + col_tool.operator("vse_toolbox.casting_add", icon="ADD", text="") + col_tool.operator("vse_toolbox.casting_remove", icon="REMOVE", text="") col_tool.separator() - col_tool.operator('vse_toolbox.casting_move', icon='TRIA_UP', text="").direction = 'UP' - col_tool.operator('vse_toolbox.casting_move', icon='TRIA_DOWN', text="").direction = 'DOWN' + col_tool.operator( + "vse_toolbox.casting_move", icon="TRIA_UP", text="" + ).direction = "UP" + col_tool.operator( + "vse_toolbox.casting_move", icon="TRIA_DOWN", text="" + ).direction = "DOWN" col_tool.separator() - col_tool.operator('vse_toolbox.copy_casting', icon='COPYDOWN', text="") - col_tool.operator('vse_toolbox.paste_casting', icon='PASTEDOWN', text="") + col_tool.operator("vse_toolbox.copy_casting", icon="COPYDOWN", text="") + col_tool.operator("vse_toolbox.paste_casting", icon="PASTEDOWN", text="") col_tool.separator() - col_tool.operator('vse_toolbox.casting_replace', icon='ZOOM_ALL', text="") + col_tool.operator("vse_toolbox.casting_replace", icon="ZOOM_ALL", text="") col_tool.separator() - col_tool.operator('vse_toolbox.casting_create_asset', icon='PLUS', text="") + col_tool.operator("vse_toolbox.casting_create_asset", icon="PLUS", text="") if strip_settings.casting: casting_item = strip_settings.casting[strip_settings.casting_index] asset = casting_item.asset - if asset: + if asset: if asset.icon_id: row = col.row(align=True) - #row.scale_y = 0.5 + # row.scale_y = 0.5 # box = col.box() # box.template_icon(icon_value=ico.icon_id, scale=7.5) @@ -335,27 +390,27 @@ class VSETB_PT_casting(VSETB_main, Panel): class VSETB_PT_metadata(VSETB_main, Panel): bl_label = "Shot Metadata" bl_parent_id = "VSETB_PT_tracker" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): return context.active_strip and get_scene_settings().active_project def draw(self, context): - + strip_settings = get_strip_settings() project = get_scene_settings().active_project layout = self.layout row = layout.row(align=True) label_col = row.column(align=True) - label_col.alignment = 'RIGHT' + label_col.alignment = "RIGHT" field_col = row.column(align=True) for metadata_type in project.metadata_types: - if metadata_type.entity_type != 'SHOT': + if metadata_type.entity_type != "SHOT": continue - + metadata_key = metadata_type.field_name metadata_label = metadata_key.title() @@ -365,24 +420,31 @@ class VSETB_PT_metadata(VSETB_main, Panel): if metadata_type.choices: metadata_value = getattr(strip_settings.metadata, metadata_key) - icon = 'LAYER_USED' + icon = "LAYER_USED" if metadata_value: if metadata_value in metadata_type.choices: - icon = 'DOT' + icon = "DOT" else: - icon = 'ADD' + icon = "ADD" + + field_row.prop_search( + strip_settings.metadata, + metadata_key, + metadata_type, + "choices", + results_are_suggestions=True, + icon=icon, + text="", + ) - field_row.prop_search(strip_settings.metadata, metadata_key, metadata_type, 'choices', - results_are_suggestions=True, icon=icon, text='') - else: - field_row.prop(strip_settings.metadata, metadata_key, text='') + field_row.prop(strip_settings.metadata, metadata_key, text="") class VSETB_PT_comments(VSETB_main, Panel): bl_label = "Comments" bl_parent_id = "VSETB_PT_tracker" - bl_options = {'DEFAULT_CLOSED'} + bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -393,17 +455,16 @@ class VSETB_PT_comments(VSETB_main, Panel): row = layout.row(align=True) label_col = row.column(align=True) - label_col.alignment = 'RIGHT' + label_col.alignment = "RIGHT" field_col = row.column(align=True) - + strip_settings = get_strip_settings() project = get_scene_settings().active_project - for task_type in project.task_types: norm_task_name = norm_str(task_type.name) - + if not hasattr(strip_settings.tasks, norm_task_name): continue @@ -412,19 +473,22 @@ class VSETB_PT_comments(VSETB_main, Panel): row = field_col.row(align=True) row.separator() sub = row.row(align=True) - sub.alignment = 'LEFT' + sub.alignment = "LEFT" sub.scale_x = 0.15 - sub.prop(task_type, 'color', text='') + sub.prop(task_type, "color", text="") sub.enabled = False - row.prop(getattr(strip_settings.tasks, norm_task_name), 'comment', text='') + row.prop(getattr(strip_settings.tasks, norm_task_name), "comment", text="") def context_menu_prop(self, context): - if not hasattr(context, 'button_prop') or context.space_data.type != 'SEQUENCE_EDITOR': + if ( + not hasattr(context, "button_prop") + or context.space_data.type != "SEQUENCE_EDITOR" + ): return - + settings = get_strip_settings() if not settings: return @@ -432,10 +496,12 @@ def context_menu_prop(self, context): button_prop = context.button_prop if button_prop not in settings.metadata.bl_rna.properties.values(): return - + layout = self.layout layout.separator() - layout.operator('vse_toolbox.copy_metadata', icon='PASTEDOWN', text='Copy metadata to selected').metadata = button_prop.name + layout.operator( + "vse_toolbox.copy_metadata", icon="PASTEDOWN", text="Copy metadata to selected" + ).metadata = button_prop.name class VSETB_MT_main_menu(Menu): @@ -444,30 +510,49 @@ class VSETB_MT_main_menu(Menu): def draw(self, context): layout = self.layout - op = layout.operator('workspace.append_activate', text='Set Review Workspace', icon="WORKSPACE") - op.idname = 'Review' + op = layout.operator( + "workspace.append_activate", text="Set Review Workspace", icon="WORKSPACE" + ) + op.idname = "Review" op.filepath = str(REVIEW_TEMPLATE_BLEND) layout.operator("wm.split_view", icon="ARROW_LEFTRIGHT") layout.separator() - layout.operator('vse_toolbox.update_media', text='Update Media', icon='FILE_REFRESH') - layout.operator('vse_toolbox.open_shot_on_tracker', text='Open Shot on Tracker', icon='URL') - layout.operator('vse_toolbox.open_strip_folder', text='Open Strip Folder', icon='FILE_FOLDER') + layout.operator( + "vse_toolbox.update_media", text="Update Media", icon="FILE_REFRESH" + ) + layout.operator( + "vse_toolbox.open_shot_on_tracker", text="Open Shot on Tracker", icon="URL" + ) + layout.operator( + "vse_toolbox.open_strip_folder", + text="Open Strip Folder", + icon="FILE_FOLDER", + ) layout.separator() - layout.operator('vse_toolbox.insert_channel', text='Insert Channel', icon='TRIA_UP_BAR') - layout.operator('vse_toolbox.remove_channel', text='Remove Channel', icon='TRIA_DOWN_BAR') + layout.operator( + "vse_toolbox.insert_channel", text="Insert Channel", icon="TRIA_UP_BAR" + ) + layout.operator( + "vse_toolbox.remove_channel", text="Remove Channel", icon="TRIA_DOWN_BAR" + ) layout.separator() - layout.operator('vse_toolbox.merge_shot_strips', text='Merge Shots') - layout.operator('vse_toolbox.create_sequence_strip', text='Create Sequence') - layout.operator('vse_toolbox.scene_cut_detection', text='Scene Cut Detection', icon='SCULPTMODE_HLT') + layout.operator("vse_toolbox.merge_shot_strips", text="Merge Shots") + layout.operator("vse_toolbox.create_sequence_strip", text="Create Sequence") + layout.operator( + "vse_toolbox.scene_cut_detection", + text="Scene Cut Detection", + icon="SCULPTMODE_HLT", + ) + def draw_vse_toolbox_menu(self, context): self.layout.menu("VSETB_MT_main_menu") - + def draw_file_new(self, context): self.layout.separator() - op = self.layout.operator('wm.read_homefile', text="Review") + op = self.layout.operator("wm.read_homefile", text="Review") op.filepath = str(REVIEW_TEMPLATE_BLEND) op.load_ui = True @@ -483,23 +568,25 @@ classes = ( VSETB_PT_presets, VSETB_PT_exports, VSETB_PT_strip, - VSETB_MT_main_menu + VSETB_MT_main_menu, ) -def register(): + +def register(): for cls in classes: bpy.utils.register_class(cls) - + bpy.types.UI_MT_button_context_menu.append(context_menu_prop) bpy.types.SEQUENCER_MT_editor_menus.append(draw_vse_toolbox_menu) bpy.types.TOPBAR_MT_file_new.append(draw_file_new) + def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) - + bpy.types.UI_MT_button_context_menu.remove(context_menu_prop) bpy.types.SEQUENCER_MT_editor_menus.remove(draw_vse_toolbox_menu) - bpy.types.TOPBAR_MT_file_new.remove(draw_file_new) \ No newline at end of file + bpy.types.TOPBAR_MT_file_new.remove(draw_file_new) diff --git a/ui/preferences.py b/ui/preferences.py index 810a1b0..203db09 100644 --- a/ui/preferences.py +++ b/ui/preferences.py @@ -64,6 +64,18 @@ def load_trackers(): print(e) +def _load_settings_after_startup(): + """Apply the config once Blender has a normal context available.""" + if not getattr(bpy.context, 'scene', None): + return 0.5 + + try: + bpy.ops.vse_toolbox.load_settings() + except RuntimeError as e: + print(f'Could not load VSE Toolbox settings at startup: {e}') + return None + + def load_prefs(): prefs = get_addon_prefs() prefs_config_file = prefs.config_path @@ -97,6 +109,11 @@ def load_prefs(): except AttributeError: pass # Scene not available yet during register() + # Loading preferences only sets the config path and tracker credentials; + # apply the project configuration once Blender has finished registering + # and provides a normal context (register() uses _RestrictContext). + bpy.app.timers.register(_load_settings_after_startup, first_interval=0.1) + class Trackers(PropertyGroup): def __iter__(self): diff --git a/ui/properties.py b/ui/properties.py index 62316e2..5e94a0e 100644 --- a/ui/properties.py +++ b/ui/properties.py @@ -119,6 +119,7 @@ class Sequence(PropertyGroup): class TaskStatus(PropertyGroup): + is_done : BoolProperty(default=False) __annotations__ = {} @@ -267,7 +268,8 @@ class ImportShots(PropertyGroup): class UploadToTracker(PropertyGroup): - render_strips: BoolProperty(default=False) + render_strips: BoolProperty(default=False, + description='Render each selected shot strip to the Movie Path before uploading it as the preview') render_strip_template : StringProperty( name="Movie Path", default="//render/{strip}.{ext}") @@ -749,7 +751,7 @@ class VSETB_PGT_strip_settings(PropertyGroup): channel = get_channel_name(strip) if channel == 'Sequences': - data = parse(project.sequence_template, strip.name) + data = parse(project.sequence_template, strip.name) or {} #data['index'] = int(data['index']) #data['sequence'] = strip.name data['strip'] = strip.name @@ -758,10 +760,10 @@ class VSETB_PGT_strip_settings(PropertyGroup): elif channel == "Shots": data = {} if sequence_strip_name := get_strip_sequence_name(strip): - data = parse(project.sequence_template, sequence_strip_name) + data = parse(project.sequence_template, sequence_strip_name) or {} data['sequence_strip'] = sequence_strip_name - data.update(parse(project.shot_template, strip.name)) + data.update(parse(project.shot_template, strip.name) or {}) #data['index'] = int(data['index']) data['strip'] = strip.name