# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors # # SPDX-License-Identifier: GPL-3.0-or-later from typing import Any, Union, List, Dict, Optional, Tuple import bpy from bpy.app.handlers import persistent from . import propsdata, bkglobals from .logger import LoggerFactory from . import cache from .types import Sequence logger = LoggerFactory.getLogger() class KITSU_sequence_colors(bpy.types.PropertyGroup): # name: StringProperty() -> Instantiated by default color: bpy.props.FloatVectorProperty( name="Sequence Color", subtype="COLOR", default=(1.0, 1.0, 1.0), min=0.0, max=1.0, description="Sequence color that will be used as line overlay", ) class KITSU_property_group_sequence(bpy.types.PropertyGroup): """ Property group that will be registered on sequence strips. They hold metadata that will be used to compose a data structure that can be pushed to backend. """ def _get_shot_description(self): return self.shot_description def _get_sequence_entity(self): try: return Sequence.by_id(self.sequence_id) except AttributeError: return None manual_shot_name: bpy.props.StringProperty( name="Shot", description="Enter a new Shot name to submit to Kitsu Server", default="", ) # type: ignore ########### # Shot ########### shot_id: bpy.props.StringProperty( # type: ignore name="Shot ID", description="ID that refers to the strip's shot on server", default="", ) def get_shot_via_name(self): return get_safely_string_prop(self, "shot_name") def set_shot_via_name(self, input): seq = self._get_sequence_entity() if seq is None: return # Attempt to set with matching Kitsu Entry kitsu_set = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_shots_enum_for_seq(self, bpy.context, seq), name_prop='shot_name', id_prop='shot_id', ) # Set manually so users can submit new shots if not kitsu_set: self['shot_name'] = input return def get_shot_search_list(self, context, edit_text): seq = self._get_sequence_entity() if seq is None: return [] return get_enum_item_names(cache.get_shots_enum_for_seq(self, bpy.context, seq)) shot_name: bpy.props.StringProperty( # type: ignore name="Shot", description="Name that refers to the strip's shot on server, Reminder: press ENTER to save shot name after entering", default="", get=get_shot_via_name, set=set_shot_via_name, options=set(), search=get_shot_search_list, search_options={'SORT', 'SUGGESTION'}, ) shot_description: bpy.props.StringProperty(name="Description", default="", options={"HIDDEN"}) # type: ignore ########### # Sequence ########### sequence_id: bpy.props.StringProperty( # type: ignore name="Seq ID", description="ID that refers to the active sequence on server", default="", ) def get_sequences_via_name(self): return get_safely_string_prop(self, "sequence_name") def set_sequences_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_sequences_enum_list(self, bpy.context), name_prop='sequence_name', id_prop='sequence_id', ) return def get_sequence_search_list(self, context, edit_text): return get_enum_item_names(cache.get_sequences_enum_list(self, bpy.context)) sequence_name: bpy.props.StringProperty( # type: ignore name="Sequence", description="Sequence", default="", get=get_sequences_via_name, set=set_sequences_via_name, options=set(), search=get_sequence_search_list, search_options={'SORT'}, ) # Project. project_name: bpy.props.StringProperty(name="Project", default="") # type: ignore project_id: bpy.props.StringProperty(name="Project ID", default="") # type: ignore # Meta. initialized: bpy.props.BoolProperty( # type: ignore name="Initialized", default=False, description="Is Kitsu shot" ) linked: bpy.props.BoolProperty( # type: ignore name="Linked", default=False, description="Is linked to an ID on server" ) # Frame range. frame_start_offset: bpy.props.IntProperty(name="Frame Start Offset") # Media. media_outdated: bpy.props.BoolProperty( name="Source Media Outdated", default=False, description="Indicated if there is a newer version of the source media available", ) # Display props. shot_description_display: bpy.props.StringProperty(name="Description", get=_get_shot_description) # type: ignore def to_dict(self): return { "id": self.id, "name": self.shot, "sequence_name": self.sequence, "description": self.description, } def clear(self): self.shot_id = "" self.shot_name = "" self.shot_description = "" self.sequence_id = "" self.sequence_name = "" self.project_name = "" self.project_id = "" self.initialized = False self.linked = False self.frame_start_offset = 0 def unlink(self): self.sequence_id = "" self.project_name = "" self.project_id = "" self.linked = False def set_kitsu_entity_id_via_enum_name( self, items: List[Tuple[str, str, str]], input_name: str, name_prop: str, id_prop: str, ) -> str: """Set the ID and Name of a Kitsu entity by finding the matching ID for a given name and updating both values in their corrisponding String Properties (Kitsu Context Properties) Args: items (List[Tuple[str, str, str]]): Enum items in the format List[Tuple[id, name, description]] input_name (str): Name, used to find matching ID in a tuple name_prop (str): Name of String Property where Kitsu Enitity's Name is stored id_prop (str): Name of String Property where Kitsu Enitity's ID is stored Returns: str: ID of Kitsu entity """ if input_name == "": self[id_prop] = "" self[name_prop] = input_name return for key, value, _ in items: if value == input_name: self[id_prop] = key self[name_prop] = input_name return key def get_enum_item_names(enum_items: List[Tuple[str, str, str]]) -> List[str]: """Return a list of names from a list of enum items used by (Kitsu Context Properties) Args: enum_items (List[Tuple[str, str, str]]): Enum items in the format List[Tuple[id, name, description]] Returns: List[str]: List of avaliable names """ return [item[1] for item in enum_items] def get_safely_string_prop(self, name: str) -> str: """Return Value of String Property, and return "" if value isn't set""" try: return self[name] except KeyError: return "" class KITSU_property_group_scene(bpy.types.PropertyGroup): """""" ################################ # Kitsu Context Properties ################################ """ Kitsu Context Properties These are properties used to store/manage the current "Context" for a Kitsu Entity. Kitsu Entities represents things like Asset, Shot and Sequence for a given production. Each Entity has an 'ID' Property which is used internally by the add-on and a 'Name' Property which is used as part of the user interface. When a user selects the 'Name' of a Kitsu Entity a custom Set function on the property will also update the Entity's ID. NOTE: It would be nice to have searchable enums instead of doing all this work manually. """ asset_col: bpy.props.PointerProperty(type=bpy.types.Collection, name="Collection") ########### # Sequence ########### sequence_active_id: bpy.props.StringProperty( # type: ignore name="Active Sequence ID", description="ID that refers to the active sequence on server", default="", ) def get_sequences_via_name(self): return get_safely_string_prop(self, "sequence_active_name") def set_sequences_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_sequences_enum_list(self, bpy.context), name_prop='sequence_active_name', id_prop='sequence_active_id', ) if key: cache.sequence_active_set_by_id(bpy.context, key) else: cache.sequence_active_reset_entity() return def get_sequence_search_list(self, context, edit_text): return get_enum_item_names(cache.get_sequences_enum_list(self, bpy.context)) sequence_active_name: bpy.props.StringProperty( name="Sequence", description="Sequence", default="", # type: ignore get=get_sequences_via_name, set=set_sequences_via_name, options=set(), search=get_sequence_search_list, search_options={'SORT'}, ) ########### # Episode ########### episode_active_id: bpy.props.StringProperty( # type: ignore name="Active Episode ID", description="ID that refers to the active episode on server", default="", ) def get_episode_via_name(self): return get_safely_string_prop(self, "episode_active_name") def set_episode_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_episodes_enum_list(self, bpy.context), name_prop='episode_active_name', id_prop='episode_active_id', ) # Clear active shot when sequence changes. if cache.episode_active_get().id != key: cache.sequence_active_reset(bpy.context) cache.asset_type_active_reset(bpy.context) cache.shot_active_reset(bpy.context) cache.asset_active_reset(bpy.context) if key: cache.episode_active_set_by_id(bpy.context, key) else: cache.episode_active_reset_entity() return def get_episode_search_list(self, context, edit_text): return get_enum_item_names(cache.get_episodes_enum_list(self, bpy.context)) episode_active_name: bpy.props.StringProperty( name="Episode", description="Selet Active Episode", default="", # type: ignore get=get_episode_via_name, set=set_episode_via_name, options=set(), search=get_episode_search_list, search_options={'SORT'}, ) ########### # Shot ########### shot_active_id: bpy.props.StringProperty( # type: ignore name="Active Shot ID", description="IDthat refers to the active shot on server", default="", # update=propsdata.on_shot_change, ) def get_shot_via_name(self): return get_safely_string_prop(self, "shot_active_name") def set_shot_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_shots_enum_for_active_seq(self, bpy.context), name_prop='shot_active_name', id_prop='shot_active_id', ) if key: cache.shot_active_set_by_id(bpy.context, key) else: cache.shot_active_reset_entity() return def get_shot_search_list(self, context, edit_text): return get_enum_item_names(cache.get_shots_enum_for_active_seq(self, bpy.context)) shot_active_name: bpy.props.StringProperty( name="Shot", description="Shot", default="", # type: ignore get=get_shot_via_name, set=set_shot_via_name, options=set(), search=get_shot_search_list, search_options={'SORT'}, ) ########### # Asset ########### asset_active_id: bpy.props.StringProperty( # type: ignore name="Active Asset ID", description="ID that refers to the active asset on server", default="", ) def get_asset_via_name(self): return get_safely_string_prop(self, "asset_active_name") def set_asset_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_assets_enum_for_active_asset_type(self, bpy.context), name_prop='asset_active_name', id_prop='asset_active_id', ) if key: cache.asset_active_set_by_id(bpy.context, key) # TODO else: cache.asset_active_reset_entity() return def get_asset_type_search_list(self, context, edit_text): return get_enum_item_names(cache.get_assets_enum_for_active_asset_type(self, bpy.context)) asset_active_name: bpy.props.StringProperty( name="Asset", description="Active Asset", default="", # type: ignore get=get_asset_via_name, set=set_asset_via_name, options=set(), search=get_asset_type_search_list, search_options={'SORT'}, ) ############ # Asset Type ############ asset_type_active_id: bpy.props.StringProperty( # type: ignore name="Active Asset Type ID", description="ID that refers to the active asset type on server", default="", ) def get_asset_type_via_name(self): return get_safely_string_prop(self, "asset_type_active_name") def set_asset_type_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_assetypes_enum_list(self, bpy.context), name_prop='asset_type_active_name', id_prop='asset_type_active_id', ) if key: cache.asset_type_active_set_by_id(bpy.context, key) else: cache.asset_type_active_reset_entity() return def get_asset_type_search_list(self, context, edit_text): return get_enum_item_names(cache.get_assetypes_enum_list(self, bpy.context)) asset_type_active_name: bpy.props.StringProperty( name="Asset Type", description="Active Asset Type Name", default="", # type: ignore get=get_asset_type_via_name, set=set_asset_type_via_name, options=set(), search=get_asset_type_search_list, search_options={'SORT'}, ) ############ # Task Type ############ task_type_active_id: bpy.props.StringProperty( # type: ignore name="Active Task Type ID", description="ID that refers to the active task type on server", default="", ) def get_task_type_via_name(self): return get_safely_string_prop(self, "task_type_active_name") def set_task_type_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_task_types_enum_for_current_context(self, bpy.context), name_prop='task_type_active_name', id_prop='task_type_active_id', ) if key: cache.task_type_active_set_by_id(bpy.context, key) else: cache.task_type_active_reset_entity() return def get_task_type_search_list(self, context, edit_text): return get_enum_item_names(cache.get_task_types_enum_for_current_context(self, bpy.context)) task_type_active_name: bpy.props.StringProperty( name="Task Type", description="Active Task Type Name", default="", # type: ignore get=get_task_type_via_name, set=set_task_type_via_name, options=set(), search=get_task_type_search_list, search_options={'SORT'}, ) category: bpy.props.EnumProperty( # type: ignore name="Type", description="Kitsu entity type", items=( ("ASSET", "Asset", "Asset related tasks", 0), ("SHOT", "Shot", "Shot related tasks", 1), ("SEQ", "Sequence", "Sequence related tasks", 2), ("EDIT", "Edit", "Edit related tasks", 3), ), default="SHOT", update=propsdata.reset_all_kitsu_props, ) ############ # Edit ############ edit_active_id: bpy.props.StringProperty( # type: ignore name="Active Edit ID", description="ID that refers to the active edit on server", default="", ) def get_edit_via_name(self): return get_safely_string_prop(self, "edit_active_name") def set_edit_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_all_edits_enum_for_active_project(self, bpy.context), name_prop='edit_active_name', id_prop='edit_active_id', ) if key: cache.edit_active_set_by_id(bpy.context, key) else: cache.edit_active_reset_entity() return def get_edit_search_list(self, context, edit_text): return get_enum_item_names(cache.get_all_edits_enum_for_active_project(self, bpy.context)) edit_active_name: bpy.props.StringProperty( name="Edit", description="Active Edit Name", default="", # type: ignore get=get_edit_via_name, set=set_edit_via_name, options=set(), search=get_edit_search_list, search_options={'SORT'}, ) edit_export_version: bpy.props.StringProperty(name="Version", default="v001") edit_export_file: bpy.props.StringProperty( # type: ignore name="Edit Export Filepath", description="Output filepath of Edit Export", default="", subtype="FILE_PATH", get=propsdata.get_edit_export_file, ) # Thumbnail props. task_type_thumbnail_id: bpy.props.StringProperty( # type: ignore name="Thumbnail Task Type ID", description="ID that refers to the task type on server for which thumbnails will be uploaded", default="", ) task_type_thumbnail_name: bpy.props.StringProperty( # type: ignore name="Thumbnail Task Type Name", description="Name that refers to the task type on server for which thumbnails will be uploaded", default="", ) # Sqe render props. task_type_sqe_render_id: bpy.props.StringProperty( # type: ignore name="Sqe Render Task Type ID", description="ID that refers to the task type on server for which the sqe render will be uploaded", default="", ) task_type_sqe_render_name: bpy.props.StringProperty( # type: ignore name="Sqe Render Task Type Name", description="Name that refers to the task type on server for which the sqe render will be uploaded", default="", ) # Playblast props. playblast_version: bpy.props.StringProperty(name="Version", default="v001") playblast_dir: bpy.props.StringProperty( # type: ignore name="Playblast Directory", description="Directory in which playblasts will be saved", default="", subtype="DIR_PATH", get=propsdata.get_playblast_dir, ) playblast_file: bpy.props.StringProperty( # type: ignore name="Playblast Filepath", description="Output filepath of playblast", default="", subtype="FILE_PATH", get=propsdata.get_playblast_file, ) playblast_task_status_id: bpy.props.StringProperty( # type: ignore name="Plablast Task Status ID", description="ID that refers to the task status on server which the playblast will set", default="", ) playblast_render_mode: bpy.props.EnumProperty( # type: ignore name="Playblast Render Mode", description="Choose to either Render Playblast from current Viewport or use scene's render settings", items=[ ( "SCENE", "Scene", "Render using the scene's render settings, playblast will match 'Render Animation's' output exactly", ), ( "VIEWPORT", "Viewport", "Render from the current viewport, with viewport shading settings and viewport overlays; what you see is what you get", ), ( "VIEWPORT_PRESET", "Viewport with Preset Shading", "Render from the current viewport with Add-On's default shading settings, and automatically hide all overlays", ), ], ) # Sequence editor tools. pull_edit_channel: bpy.props.IntProperty( name="Channel", description="On which channel the operator will create the color strips", default=1, min=1, max=32, ) sequence_colors: bpy.props.CollectionProperty(type=KITSU_sequence_colors) class KITSU_property_group_error(bpy.types.PropertyGroup): """""" frame_range: bpy.props.BoolProperty( # type: ignore name="Frame Range Error", description="Indicates if the scene frame range does not match the one in Kitsu", default=False, ) class KITSU_property_group_window_manager(bpy.types.PropertyGroup): """""" tasks_index: bpy.props.IntProperty(name="Tasks Index", default=0) quick_duplicate_amount: bpy.props.IntProperty( name="Amount", default=1, min=1, description="Specifies the number of copies that will be created", ) def clear(self): pass def _add_window_manager_props(): # Multi Edit Properties. bpy.types.WindowManager.show_advanced = bpy.props.BoolProperty( name="Show Advanced", description="Shows advanced options to fine control shot pattern", default=False, ) bpy.types.WindowManager.var_use_custom_seq = bpy.props.BoolProperty( name="Use Custom", description="Enables to type in custom sequence name for wildcard", default=False, ) bpy.types.WindowManager.var_use_custom_project = bpy.props.BoolProperty( name="Use Custom", description="Enables to type in custom project name for wildcard", default=False, ) bpy.types.WindowManager.var_sequence_custom = bpy.props.StringProperty( # type: ignore name="Custom Sequence Variable", description="Value that will be used to insert in wildcard if custom sequence is enabled", default="", ) bpy.types.WindowManager.var_project_custom = bpy.props.StringProperty( # type: ignore name="Custom Project Variable", description="Value that will be used to insert in wildcard if custom project is enabled", default="", ) bpy.types.WindowManager.shot_counter_start = bpy.props.IntProperty( description="Value that defines where the shot counter starts", step=10, min=0, ) bpy.types.WindowManager.shot_preview = bpy.props.StringProperty( name="Shot Pattern", description="Preview result of current settings on how a shot will be named", get=propsdata._gen_shot_preview, ) bpy.types.WindowManager.var_project_active = bpy.props.StringProperty( name="Active Project", description="Value that will be inserted in wildcard", get=propsdata._get_project_active, ) ########### # Sequence ########### bpy.types.WindowManager.selected_sequence_id = bpy.props.StringProperty( # type: ignore name="Active Sequence ID", description="ID that refers to the active sequence on server", default="", ) def get_sequences_via_name(self): return get_safely_string_prop(self, "selected_sequence_name") def set_sequences_via_name(self, input): key = set_kitsu_entity_id_via_enum_name( self=self, input_name=input, items=cache.get_sequences_enum_list(self, bpy.context), name_prop='selected_sequence_name', id_prop='selected_sequence_id', ) if key: cache.sequence_active_set_by_id(bpy.context, key) else: cache.sequence_active_reset_entity() return def get_sequence_search_list(self, context, edit_text): return get_enum_item_names(cache.get_sequences_enum_list(self, bpy.context)) bpy.types.WindowManager.selected_sequence_name = bpy.props.StringProperty( name="Sequence", description="Name of Sequence the generated Shots will be assinged to", default="", # type: ignore get=get_sequences_via_name, set=set_sequences_via_name, options=set(), search=get_sequence_search_list, search_options={'SORT'}, ) # Advanced delete props. bpy.types.WindowManager.advanced_delete = bpy.props.BoolProperty( name="Advanced Delete", description="Checkbox to show advanced shot deletion operations", default=False, ) def _clear_window_manager_props(): del bpy.types.WindowManager.show_advanced del bpy.types.WindowManager.var_use_custom_seq del bpy.types.WindowManager.var_use_custom_project del bpy.types.WindowManager.var_sequence_custom del bpy.types.WindowManager.var_project_custom del bpy.types.WindowManager.shot_counter_start del bpy.types.WindowManager.shot_preview del bpy.types.WindowManager.var_project_active del bpy.types.WindowManager.selected_sequence_id del bpy.types.WindowManager.selected_sequence_name def _calc_kitsu_3d_start(self): """ Calculates strip.kitsu_3d_start, little hack because it seems like we cant access the strip from a property group But we need acess to seqeuence properties. """ return int(self.frame_final_start - self.frame_start + bkglobals.FRAME_START) def _calc_kitsu_frame_end(self): """ Calculates strip.kitsu_frame_end, little hack because it seems like we cant access the strip from a property group But we need acess to seqeuence properties. """ frame_start = _calc_kitsu_3d_start(self) frame_end_final = frame_start + (self.frame_final_duration - 1) return int(frame_end_final) def _get_frame_final_duration(self): return self.frame_final_duration def _get_strip_filepath(self): return self.filepath @persistent def update_sequence_colors_coll_prop(dummy: Any) -> None: sqe = bpy.context.scene.sequence_editor if not sqe: return sequences = sqe.sequences_all sequence_colors = bpy.context.scene.kitsu.sequence_colors existings_seq_ids: List[str] = [] # Append missing sequences to scene.kitsu.seqeuence_colors. for seq in sequences: if not seq.kitsu.sequence_id: continue if seq.kitsu.sequence_id not in sequence_colors.keys(): logger.info("Added %s to scene.kitsu.seqeuence_colors", seq.kitsu.sequence_name) item = sequence_colors.add() item.name = seq.kitsu.sequence_id existings_seq_ids.append(seq.kitsu.sequence_id) # Delete sequence colors that are not in edit anymore. existings_seq_ids = set(existings_seq_ids) to_be_removed = [seq_id for seq_id in sequence_colors.keys() if seq_id not in existings_seq_ids] for seq_id in to_be_removed: idx = sequence_colors.find(seq_id) if idx == -1: continue sequence_colors.remove(idx) logger.info( "Removed %s from scene.kitsu.seqeuence_colors. Is not used in the sequence editor anymore", seq_id, ) # ----------------REGISTER--------------. classes = [ KITSU_sequence_colors, KITSU_property_group_sequence, KITSU_property_group_scene, KITSU_property_group_error, KITSU_property_group_window_manager, ] def register(): for cls in classes: bpy.utils.register_class(cls) # FRAME RANGE PROPERTIES # because we cant acess strip properties from a sequence group we need to create this properties # directly on the strip, as we need strip properties to calculate bpy.types.Strip.kitsu_3d_start = bpy.props.IntProperty( name="3D In", get=_calc_kitsu_3d_start, ) bpy.types.Strip.kitsu_frame_end = bpy.props.IntProperty( name="3D Out", get=_calc_kitsu_frame_end, ) bpy.types.Strip.kitsu_frame_duration = bpy.props.IntProperty( name="Duration", get=_get_frame_final_duration, ) # Used in general tools panel next to sqe_change_strip_source operator. bpy.types.MovieStrip.filepath_display = bpy.props.StringProperty( name="Filepath Display", get=_get_strip_filepath ) # Sequence Properties. bpy.types.Strip.kitsu = bpy.props.PointerProperty( name="Kitsu", type=KITSU_property_group_sequence, description="Metadata that is required for blender_kitsu", ) # Scene Properties. bpy.types.Scene.kitsu = bpy.props.PointerProperty( name="Kitsu", type=KITSU_property_group_scene, description="Metadata that is required for blender_kitsu", ) # Window Manager. bpy.types.WindowManager.kitsu = bpy.props.PointerProperty( name="Kitsu", type=KITSU_property_group_window_manager, description="Metadata that is required for blender_kitsu", ) # Error Properties. bpy.types.Scene.kitsu_error = bpy.props.PointerProperty( name="Kitsu Error", type=KITSU_property_group_error, description="Error property group", ) # Window Manager Properties. _add_window_manager_props() # Handlers. bpy.app.handlers.load_post.append(update_sequence_colors_coll_prop) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) # Clear properties. _clear_window_manager_props() # Handlers. bpy.app.handlers.load_post.remove(update_sequence_colors_coll_prop)