Blender Kitsu: Use new Kitsu Context UI in Multi Edit / Metadata #280

Merged
Nick Alberelli merged 11 commits from TinyNick/blender-studio-pipeline:fix/blender-kitsu-multi-edit into main 2024-04-08 17:51:04 +02:00
8 changed files with 211 additions and 60 deletions

Binary file not shown.

Binary file not shown.

View File

@ -13,7 +13,7 @@ blender-kitsu is a Blender Add-on to interact with Kitsu from within Blender. It
- [Metadata](#metadata)
- [Push](#push)
- [Pull](#pull)
- [Multi Edit](#multi-edit)
- [Multi Edit Metadata](#multi-edit-metadata)
- [Import Media](#import-media)
- [General Sequence Editor Tools](#general-sequence-editor-tools)
- [Context](#context)
@ -185,13 +185,25 @@ As a result a bigger edit with nice sequence_colors can look pretty cool:
![image info](/media/addons/blender_kitsu/sqe_sequence_colors.jpg)
##### Multi Edit
##### Multi Edit Metadata
The `Multi Edit` panel only appears when you select multiple metadata strips that are all `initialized` but not `linked` yet. <br/>
The `Multi Edit Metadata` panel only appears when you select multiple metadata strips that are all `initialized` but not `linked` yet. <br/>
![image info](/media/addons/blender_kitsu/sqe_multi_edit.jpg)
It is meant to be way to quickly setup lots of shots if they don't exist on Kitsu yet. You specify the sequence all shots should belong to and adjust the `Shot Counter Start` value. In the preview property you can see how all shots will be named when you execute the `Multi Edit Strip` operator. <br/>
###### Advanced Settings
If you enable the `Advanced` mode (via the screwdriver/wrench icon) next to the counter value, you have access to advance settings to customize the operator even more.
![image info](/media/addons/blender_kitsu/sqe_multi_edit_advanced.jpg)
You can adjust the number of counter digits, the increment size and also the `Pattern` it will use to generate the shot name. <br/>
>**Pattern**: supports 3 wildcards. `<Sequence>`, `<Counter>`, `<Project>`, `Episode` that can be used multiple times in any order. <br/>
**Custom Sequence Variable**: specify a custom string that should be used in the `<Sequence>` wildcard instead of the sequence name. <br/>
**Custom Project Variable**: specify a custom string that should be used in the `<Project>` wildcard instead of the project name. <br/>
##### Import Media
A collection of operators to Import media based on the Shot associated with the selected metadata strip(s). <br/>
@ -208,16 +220,6 @@ With a metadata strip selected `Import Image Sequence` Operator will find an ima
Use this operator to import image sequences that have been approved via the [Render Review Add-On](/addons/render_review) Image Sequences can be loaded as either `EXR` or `JPG` sequences.
###### Advanced Settings
If you check the `Advanced` checkbox next to the counter value, you have access to advance settings to customize the operator even more.
![image info](/media/addons/blender_kitsu/sqe_multi_edit_advanced.jpg)
You can adjust the number of counter digits, the increment size and also the `Pattern` it will use to generate the shot name. <br/>
>**Pattern**: supports 3 wildcards. `<Sequence>`, `<Counter>`, `<Project>` that can be used multiple times in any order. <br/>
**Custom Sequence Variable**: specify a custom string that should be used in the `<Sequence>` wildcard instead of the sequence name. <br/>
**Custom Project Variable**: specify a custom string that should be used in the `<Project>` wildcard instead of the project name. <br/>
##### General Sequence Editor Tools
In the general tab you can find some tools that don't directly relate to Kitsu but are useful for editing.

View File

@ -316,10 +316,26 @@ class KITSU_addon_preferences(bpy.types.AddonPreferences):
description="Show advanced settings that should already have good defaults",
)
def set_shot_pattern(self, input):
self['shot_pattern'] = input
return
def get_shot_pattern(
self,
) -> str:
active_project = cache.project_active_get()
if get_safely_string_prop(self, 'shot_pattern') == "":
if active_project.production_type == bkglobals.KITSU_TV_PROJECT:
return "<Episode>_<Sequence>_<Counter>"
return "<Sequence>_<Counter>"
return get_safely_string_prop(self, 'shot_pattern')
shot_pattern: bpy.props.StringProperty( # type: ignore
name="Shot Pattern",
description="Pattern to define how Bulk Init will name the shots. Supported wildcards: <Project>, <Sequence>, <Counter>",
default="<Sequence>_<Counter>_A",
description="Pattern to define how Bulk Init will name the shots. Supported wildcards: <Project>, <Episode>, <Sequence>, <Counter>",
default="<Sequence>_<Counter>",
get=get_shot_pattern,
set=set_shot_pattern,
)
shot_counter_digits: bpy.props.IntProperty( # type: ignore

View File

@ -27,6 +27,7 @@ from bpy.app.handlers import persistent
from . import propsdata, bkglobals
from .logger import LoggerFactory
from . import cache
from .types import Sequence
logger = LoggerFactory.getLogger()
@ -49,21 +50,98 @@ class KITSU_property_group_sequence(bpy.types.PropertyGroup):
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_name(self):
return self.sequence_name
def _get_sequence_entity(self):
try:
return Sequence.by_id(self.sequence_id)
except AttributeError:
return None
# Shot.
shot_id: bpy.props.StringProperty(name="Shot ID") # type: ignore
shot_name: bpy.props.StringProperty(name="Shot", 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
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',
)
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",
default="",
get=get_shot_via_name,
set=set_shot_via_name,
options=set(),
search=get_shot_search_list,
search_options={'SORT'},
)
shot_description: bpy.props.StringProperty(name="Description", default="", options={"HIDDEN"}) # type: ignore
# Sequence.
sequence_name: bpy.props.StringProperty(name="Sequence", default="") # type: ignore
sequence_id: bpy.props.StringProperty(name="Seq ID", default="") # 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
@ -89,7 +167,6 @@ class KITSU_property_group_sequence(bpy.types.PropertyGroup):
# Display props.
shot_description_display: bpy.props.StringProperty(name="Description", get=_get_shot_description) # type: ignore
sequence_name_display: bpy.props.StringProperty(name="Sequence", get=_get_sequence_name) # type: ignore
def to_dict(self):
return {
@ -266,6 +343,8 @@ class KITSU_property_group_scene(bpy.types.PropertyGroup):
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):
@ -670,10 +749,44 @@ def _add_window_manager_props():
get=propsdata._get_project_active,
)
bpy.types.WindowManager.sequence_enum = bpy.props.EnumProperty(
name="Sequences",
items=propsdata._get_sequences,
###########
# 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.
@ -693,7 +806,8 @@ def _clear_window_manager_props():
del bpy.types.WindowManager.shot_counter_start
del bpy.types.WindowManager.shot_preview
del bpy.types.WindowManager.var_project_active
del bpy.types.WindowManager.sequence_enum
del bpy.types.WindowManager.selected_sequence_id
del bpy.types.WindowManager.selected_sequence_name
def _calc_kitsu_3d_start(self):

View File

@ -77,12 +77,17 @@ def _gen_shot_preview(self: Any) -> str:
shot_counter_start = self.shot_counter_start
shot_pattern = addon_prefs.shot_pattern
examples: List[str] = []
sequence = self.sequence_enum
sequence = self.selected_sequence_name
episode = cache.episode_active_get()
var_project = (
self.var_project_custom if self.var_use_custom_project else self.var_project_active
)
var_sequence = self.var_sequence_custom if self.var_use_custom_seq else sequence
var_lookup_table = {"Sequence": var_sequence, "Project": var_project}
var_lookup_table = {
"Sequence": var_sequence,
"Project": var_project,
"Episode": episode.name,
}
for count in range(3):
counter_number = shot_counter_start + (shot_counter_increment * count)

View File

@ -528,8 +528,6 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator):
bl_description = "Links selected sequence strip to shot on server. Pulls all metadata of shot from server"
bl_options = {"REGISTER", "UNDO"}
sequence_enum: bpy.props.EnumProperty(items=cache.get_sequences_enum_list, name="Sequence") # type: ignore
shots_enum: bpy.props.EnumProperty(items=opsdata.get_shots_enum_for_link_shot_op, name="Shot") # type: ignore
use_url: bpy.props.BoolProperty(
name="Use URL",
description="Use URL of shot on server to initiate strip. Paste complete URL",
@ -540,6 +538,8 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator):
default="",
)
_strip = None
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor
@ -555,9 +555,8 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator):
)
def execute(self, context: bpy.types.Context) -> Set[str]:
strip = context.scene.sequence_editor.active_strip
shot_id = self.shots_enum
shot_id = self._strip.kitsu.shot_id
# By url.
if self.use_url:
@ -567,8 +566,8 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator):
# By shot enum.
else:
shot_id = self.shots_enum
if not shot_id:
shot_id = self._strip.kitsu.shot_id
if shot_id == "":
self.report({"WARNING"}, "Invalid selection. Please choose a shot")
return {"CANCELLED"}
@ -585,10 +584,10 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator):
return {"CANCELLED"}
# Pull shot meta.
pull.shot_meta(strip, shot)
pull.shot_meta(self._strip, shot)
# Rename strip.
strip.name = shot.name
self._strip.name = shot.name
# Pull sequence color.
seq = Sequence.by_id(shot.parent_id)
@ -596,7 +595,7 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator):
# Log.
t = "Linked strip: %s to shot: %s with ID: %s" % (
strip.name,
self._strip.name,
shot.name,
shot.id,
)
@ -609,6 +608,7 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator):
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
if context.window_manager.clipboard:
self.url = context.window_manager.clipboard
self._strip = context.scene.sequence_editor.active_strip
return context.window_manager.invoke_props_dialog( # type: ignore
self, width=400
@ -624,9 +624,9 @@ class KITSU_OT_sqe_link_shot(bpy.types.Operator):
row.prop(self, "url", text="")
else:
row = layout.row()
row.prop(self, "sequence_enum")
row.prop(self._strip.kitsu, "sequence_name")
row = layout.row()
row.prop(self, "shots_enum")
row.prop(self._strip.kitsu, "shot_name")
row = layout.row()
@ -647,10 +647,12 @@ class KITSU_OT_sqe_multi_edit_strip(bpy.types.Operator):
# and they all have the same sequence name.
sel_shots = context.selected_sequences
if not sel_shots:
cls.poll_message_set("No sequences are selected")
return False
nr_of_shots = len(sel_shots)
if nr_of_shots < 1:
cls.poll_message_set("Please more than one sequence")
return False
seq_name = sel_shots[0].kitsu.sequence_name
@ -660,8 +662,14 @@ class KITSU_OT_sqe_multi_edit_strip(bpy.types.Operator):
or not s.kitsu.initialized
or not checkstrip.is_valid_type(s)
):
cls.poll_message_set(
"Please select unlinked, initialized strips, of either MOVIE or COLOR type"
)
return False
if s.kitsu.sequence_name != seq_name:
cls.poll_message_set(
"Strips have conflicting sequence names. Please select strips with the same sequence name, or no sequence"
)
return False
return True
@ -672,7 +680,7 @@ class KITSU_OT_sqe_multi_edit_strip(bpy.types.Operator):
shot_counter_start = context.window_manager.shot_counter_start
shot_pattern = addon_prefs.shot_pattern
strip = context.scene.sequence_editor.active_strip
sequence = context.window_manager.sequence_enum
sequence = context.window_manager.selected_sequence_name
var_project = (
addon_prefs.var_project_custom
if context.window_manager.var_use_custom_project
@ -692,12 +700,14 @@ class KITSU_OT_sqe_multi_edit_strip(bpy.types.Operator):
selected_sequences = sorted(
selected_sequences, key=lambda x: x.frame_final_start
)
episode = cache.episode_active_get()
for idx, strip in enumerate(selected_sequences):
# Gen data for resolver.
counter_number = shot_counter_start + (shot_counter_increment * idx)
counter = str(counter_number).rjust(shot_counter_digits, "0")
var_lookup_table = {
"Episode": episode.name,
"Sequence": var_sequence,
"Project": var_project,
"Counter": counter,
@ -708,7 +718,9 @@ class KITSU_OT_sqe_multi_edit_strip(bpy.types.Operator):
# Set metadata.
strip.kitsu.sequence_name = sequence
strip.kitsu.sequence_id = context.window_manager.selected_sequence_id
strip.kitsu.shot_name = shot
strip.name = shot
succeeded.append(strip)
logger.info(

View File

@ -20,7 +20,7 @@
import bpy
from .. import cache, prefs, ui
from .. import cache, prefs, ui, bkglobals
from ..sqe import checkstrip
from ..context import core as context_core
from ..logger import LoggerFactory
@ -105,6 +105,11 @@ class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
return bool(prefs.session_auth(context) or (sqe and sqe.sequences_all))
def draw(self, context: bpy.types.Context) -> None:
active_project = cache.project_active_get()
if active_project.production_type == bkglobals.KITSU_TV_PROJECT:
if not cache.episode_active_get():
self.layout.label(text="Please Set Active Episode", icon="ERROR")
return
if self.poll_error(context):
self.draw_error(context)
@ -300,17 +305,13 @@ class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
if not strip.kitsu.sequence_id:
sub_row = split.row(align=True)
sub_row.prop(strip.kitsu, "sequence_name_display", text="")
sub_row.operator(KITSU_OT_sqe_link_sequence.bl_idname, text="", icon="DOWNARROW_HLT")
sub_row.prop(strip.kitsu, "sequence_name", text="")
sub_row.operator(KITSU_OT_sqe_push_new_sequence.bl_idname, text="", icon="ADD")
else:
# Lots of splitting because color prop is too big by default
sub_split = split.split(factor=0.6, align=True)
sub_split.prop(strip.kitsu, "sequence_name_display", text="")
sub_split = sub_split.split(factor=0.3, align=True)
sub_split.operator(KITSU_OT_sqe_link_sequence.bl_idname, text="", icon="DOWNARROW_HLT")
sub_split = split.split(factor=0.8, align=True)
sub_split.prop(strip.kitsu, "sequence_name", text="") # TODO Use new dropdown here too
sub_sub_split = sub_split.split(factor=0.4, align=True)
sub_sub_split.operator(KITSU_OT_sqe_push_new_sequence.bl_idname, text="", icon="ADD")
@ -354,6 +355,8 @@ class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
@classmethod
def poll_multi_edit(cls, context: bpy.types.Context) -> bool:
if not prefs.session_auth(context):
return False
sel_shots = context.selected_sequences
nr_of_shots = len(sel_shots)
unvalid = [s for s in sel_shots if s.kitsu.linked or not s.kitsu.initialized]
@ -366,26 +369,25 @@ class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
"""
addon_prefs = prefs.addon_prefs_get(context)
nr_of_shots = len(context.selected_sequences)
noun = get_selshots_noun(nr_of_shots)
# Create box.
layout = self.layout
box = layout.box()
box.label(text="Multi Edit", icon="PROPERTIES")
box.label(text="Multi Edit Metadata", icon="PROPERTIES")
# Sequence
# TODO: use link sequence operator instead or sequence_enum ?
col = box.column()
sub_row = col.row(align=True)
# Sub_row.prop(context.window_manager, "sequence_name_display").
sub_row.prop(context.window_manager, "sequence_enum", text="Sequence")
sub_row.prop(context.window_manager, "selected_sequence_name", text="Sequence")
sub_row.operator(KITSU_OT_sqe_push_new_sequence.bl_idname, text="", icon="ADD")
# Counter.
row = box.row()
row.prop(context.window_manager, "shot_counter_start", text="Shot Counter Start")
row.prop(context.window_manager, "show_advanced", text="")
row.prop(context.window_manager, "show_advanced", text="", icon="TOOL_SETTINGS")
if context.window_manager.show_advanced:
# Counter.
@ -422,8 +424,8 @@ class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
row = box.row(align=True)
row.operator(
KITSU_OT_sqe_multi_edit_strip.bl_idname,
text=f"Edit {noun}",
icon="TRIA_RIGHT",
text=f"Set Metadata for {noun}",
icon="ALIGN_LEFT",
)
@classmethod
@ -445,7 +447,7 @@ class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
strips_to_tb.append(s)
strips_to_meta.append(s)
elif s.kitsu.initialized:
elif s.kitsu.initialized and s.kitsu.shot_id != "":
strips_to_submit.append(s)
return bool(strips_to_meta or strips_to_tb or strips_to_submit)