2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import importlib
from ..sqe import opsdata, checkstrip, pull, push, ops, ui, draw
# ---------REGISTER ----------.
def reload():
global opsdata
global checkstrip
global pull
global push
global ops
global ui
global draw
opsdata = importlib.reload(opsdata)
checkstrip = importlib.reload(checkstrip)
pull = importlib.reload(pull)
push = importlib.reload(push)
ops = importlib.reload(ops)
ui = importlib.reload(ui)
draw = importlib.reload(draw)
def register():
ops.register()
ui.register()
draw.register()
def unregister():
ui.unregister()
ops.unregister()
draw.unregister()
@@ -0,0 +1,72 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Dict, List
import bpy
from ..logger import LoggerFactory
from ..sqe import checkstrip
logger = LoggerFactory.getLogger()
def _is_range_in(range1: range, range2: range) -> bool:
"""Whether range1 is a subset of range2"""
# usual strip setup strip1(101, 120)|strip2(120, 130)|strip3(130, 140)
# first and last frame can be the same for each strip
range2 = range(range2.start + 1, range2.stop - 1)
if not range1:
return True # empty range is subset of anything
if not range2:
return False # non-empty range can't be subset of empty range
if len(range1) > 1 and range1.step % range2.step:
return False # must have a single value or integer multiple step
return range1.start in range2 or range1[-1] in range2
def get_occupied_ranges(context: bpy.types.Context) -> Dict[str, List[range]]:
"""
Scans sequence editor and returns a dictionary. It contains a key for each channel
and a list of ranges with the occupied frame ranges as values.
"""
# {'1': [(101, 213), (300, 320)]}.
ranges: Dict[str, List[range]] = {}
# Populate ranges.
for strip in context.scene.sequence_editor.sequences_all:
ranges.setdefault(str(strip.channel), [])
ranges[str(strip.channel)].append(
range(strip.frame_final_start, strip.frame_final_end + 1)
)
# Sort ranges tuple list.
for channel in ranges:
liste = ranges[channel]
ranges[channel] = sorted(liste, key=lambda item: item.start)
return ranges
def is_range_occupied(range_to_check: range, occupied_ranges: List[range]) -> bool:
for r in occupied_ranges:
# Range(101, 150).
if _is_range_in(range_to_check, r):
return True
continue
return False
def get_shot_strips(context: bpy.types.Context) -> List[bpy.types.Strip]:
shot_strips = []
shot_strips.extend(
[
strip
for strip in context.scene.sequence_editor.sequences_all
if checkstrip.is_valid_type(strip, log=False)
and checkstrip.is_linked(strip, log=False)
]
)
return shot_strips
@@ -0,0 +1,131 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Optional
import bpy
import gazu
from ..types import Sequence, Project, Shot, Cache
from ..logger import LoggerFactory
logger = LoggerFactory.getLogger()
VALID_STRIP_TYPES = {"MOVIE", "COLOR"}
def is_valid_type(strip: bpy.types.Strip, log: bool = True) -> bool:
if not strip.type in VALID_STRIP_TYPES:
if log:
logger.info("Strip: %s. Invalid type", strip.type)
return False
return True
def is_initialized(strip: bpy.types.Strip) -> bool:
"""Returns True if strip.kitsu.initialized is True else False"""
if not strip.kitsu.initialized:
logger.info("Strip: %s. Not initialized", strip.name)
return False
logger.info("Strip: %s. Is initialized", strip.name)
return True
def is_linked(strip: bpy.types.Strip, log: bool = True) -> bool:
"""Returns True if strip.kitsu.linked is True else False"""
if not strip.kitsu.linked:
if log:
logger.info("Strip: %s. Not linked yet", strip.name)
return False
if log:
logger.info("Strip: %s. Is linked to ID: %s", strip.name, strip.kitsu.shot_id)
return True
def has_meta(strip: bpy.types.Strip) -> bool:
"""Returns True if strip.kitsu.shot_name and strip.kitsu.sequence_name is Truethy else False"""
seq = strip.kitsu.sequence_name
shot = strip.kitsu.shot_name or strip.kitsu.manual_shot_name
if not bool(seq and shot):
logger.info("Strip: %s. Missing metadata", strip.name)
return False
logger.info("Strip: %s. Has metadata (Sequence: %s, Shot: %s)", strip.name, seq, shot)
return True
def shot_exists_by_id(strip: bpy.types.Strip, clear_cache: bool = True) -> Optional[Shot]:
"""Returns Shot instance if shot with strip.kitsu.shot_id exists else None"""
if clear_cache:
Cache.clear_all()
try:
shot = Shot.by_id(strip.kitsu.shot_id)
except (gazu.exception.RouteNotFoundException, gazu.exception.ServerErrorException):
logger.info(
"Strip: %s No shot found on server with ID: %s",
strip.name,
strip.kitsu.shot_id,
)
return None
logger.info("Strip: %s Shot %s exists on server (ID: %s)", strip.name, shot.name, shot.id)
return shot
def seq_exists_by_id(
strip: bpy.types.Strip, project: Project, clear_cache: bool = True
) -> Optional[Sequence]:
if clear_cache:
Cache.clear_all()
zseq = project.get_sequence(strip.kitsu.sequence_id)
if not zseq:
logger.info(
"Strip: %s Sequence %s does not exist on server",
strip.name,
strip.kitsu.sequence_name,
)
return None
logger.info(
"Strip: %s Sequence %s exists in on server (ID: %s)",
strip.name,
zseq.name,
zseq.id,
)
return zseq
def shot_exists_by_name(
strip: bpy.types.Strip,
project: Project,
sequence: Sequence,
clear_cache: bool = True,
) -> Optional[Shot]:
"""Returns Shot instance if strip.kitsu.shot_name exists on server, else None"""
if clear_cache:
Cache.clear_all()
shot = project.get_shot_by_name(sequence, strip.kitsu.manual_shot_name)
if not shot:
logger.info(
"Strip: %s Shot %s does not exist on server",
strip.name,
strip.kitsu.manual_shot_name,
)
return None
logger.info("Strip: %s Shot already existent on server (ID: %s)", strip.name, shot.id)
return shot
def contains(strip: bpy.types.Strip, framenr: int) -> bool:
"""Returns True if the strip covers the given frame number"""
return int(strip.frame_final_start) <= framenr <= int(strip.frame_final_end)
+126
View File
@@ -0,0 +1,126 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import typing
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
# Shaders and batches
rect_coords = ((0, 0), (1, 0), (1, 1), (0, 1))
indices = ((0, 1, 2), (2, 3, 0))
# Setup shaders only if Blender runs in the foreground.
# If running in the background, no handles are registered, as drawing extra UI
# elements does not make sense.
# See register() and unregister().
if not bpy.app.background:
ucolor_2d_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
ucolor_2d_rect_batch = batch_for_shader(
ucolor_2d_shader, "TRIS", {"pos": rect_coords}, indices=indices
)
Float2 = typing.Tuple[float, float]
Float3 = typing.Tuple[float, float, float]
Float4 = typing.Tuple[float, float, float, float]
def draw_line(position: Float2, size: Float2, color: Float4):
with gpu.matrix.push_pop():
gpu.state.blend_set("ALPHA")
gpu.matrix.translate(position)
gpu.matrix.scale(size)
ucolor_2d_shader.uniform_float("color", color)
ucolor_2d_rect_batch.draw(ucolor_2d_shader)
gpu.state.blend_set("NONE")
def get_strip_rectf(strip) -> Float4:
# Get x and y in terms of the grid's frames and channels.
x1 = strip.frame_final_start
x2 = strip.frame_final_end
# Seems to be a 5 % offset from channel top start of strip.
y1 = strip.channel + 0.05
y2 = strip.channel - 0.05 + 1
return x1, y1, x2, y2
def draw_line_in_strip(strip_coords: Float4, height_factor: float, color: Float4):
# Unpack strip coordinates.
s_x1, channel, s_x2, _ = strip_coords
# Get the line's measures, as a percentage of channel height.
# Note that the strip's height is 10% smaller than the channel (centered 5% top and bottom).
# Note 2: offset the height slightly (0.005) to make room for the selection outline (channel coords).
line_height_in_channel = (0.9 - 0.005 * 2) * height_factor + 0.005
line_thickness = 0.04
# Offset the width slightly to make room for the selection outline (virtual grid horizontal coords).
width_offset = 0.2
width = (s_x2 - s_x1) - width_offset * 2
pos = (s_x1 + width_offset, channel + line_height_in_channel)
scale = (width, line_thickness)
draw_line(pos, scale, color)
def draw_callback_px():
context = bpy.context
sqe = context.scene.sequence_editor
if not sqe:
return
strips = sqe.sequences_all
for strip in strips:
# Get corners of the strip rectangle in terms of the grid's frames and channels (virtual, not px).
strip_coords = get_strip_rectf(strip)
if strip.kitsu.initialized or strip.kitsu.linked:
try:
color = tuple(
context.scene.kitsu.sequence_colors[strip.kitsu.sequence_id].color
)
except KeyError:
color = (1, 1, 1)
alpha = 0.75 if strip.kitsu.linked else 0.25
line_color = color + (alpha,)
draw_line_in_strip(strip_coords, 0.0, line_color)
if strip.kitsu.media_outdated:
line_color = (1.0, 0.05, 0.145, 0.75)
draw_line_in_strip(strip_coords, 0.9, line_color)
draw_handles = []
def register():
if bpy.app.background:
# Do not register anything if Blender runs in the background (no UI needed).
return
draw_handles.append(
bpy.types.SpaceSequenceEditor.draw_handler_add(
draw_callback_px, (), "WINDOW", "POST_VIEW"
)
)
def unregister():
if bpy.app.background:
return
for handle in reversed(draw_handles):
try:
bpy.types.SpaceSequenceEditor.draw_handler_remove(handle, "WINDOW")
except ValueError:
# Not sure why, but sometimes the handler seems to already be removed...??
pass
File diff suppressed because it is too large Load Diff
+265
View File
@@ -0,0 +1,265 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import re
from pathlib import Path
from typing import Any, Dict, List, Tuple, Union, Optional
from . import pull
import bpy
from .. import bkglobals, prefs
from ..logger import LoggerFactory
from ..types import Sequence, Task, TaskStatus, Shot, TaskType
logger = LoggerFactory.getLogger()
_sqe_shot_enum_list: List[Tuple[str, str, str]] = []
_sqe_not_linked: List[Tuple[str, str, str]] = []
_sqe_duplicates: List[Tuple[str, str, str]] = []
_sqe_multi_project: List[Tuple[str, str, str]] = []
def sqe_get_not_linked(self, context):
return _sqe_not_linked
def sqe_get_duplicates(self, context):
return _sqe_duplicates
def sqe_get_multi_project(self, context):
return _sqe_multi_project
def sqe_update_not_linked(context: bpy.types.Context) -> List[Tuple[str, str, str]]:
"""get all strips that are initialized but not linked yet"""
enum_list = []
if context.selected_sequences:
strips = context.selected_sequences
else:
strips = context.scene.sequence_editor.sequences_all
for strip in strips:
if strip.kitsu.initialized and not strip.kitsu.linked:
enum_list.append((strip.name, strip.name, ""))
return enum_list
def sqe_update_duplicates(context: bpy.types.Context) -> List[Tuple[str, str, str]]:
"""get all strips that are initialized but not linked yet"""
enum_list = []
data_dict = {}
if context.selected_sequences:
strips = context.selected_sequences
else:
strips = context.scene.sequence_editor.sequences_all
# Create data dict that holds all shots ids and the corresponding strips that are linked to it.
for i in range(len(strips)):
if strips[i].kitsu.linked:
# Get shot_id, shot_name, create entry in data_dict if id not existent.
shot_id = strips[i].kitsu.shot_id
shot_name = strips[i].kitsu.shot_name
if shot_id not in data_dict:
data_dict[shot_id] = {"name": shot_name, "strips": []}
# Append i to strips list.
if strips[i] not in set(data_dict[shot_id]["strips"]):
data_dict[shot_id]["strips"].append(strips[i])
# Comparet to all other strip.
for j in range(i + 1, len(strips)):
if shot_id == strips[j].kitsu.shot_id:
data_dict[shot_id]["strips"].append(strips[j])
# Convert in data strucutre for enum property.
for shot_id, data in data_dict.items():
if len(data["strips"]) > 1:
enum_list.append(("", data["name"], shot_id))
for strip in data["strips"]:
enum_list.append((strip.name, strip.name, ""))
return enum_list
def sqe_update_multi_project(context: bpy.types.Context) -> List[Tuple[str, str, str]]:
"""get all strips that are initialized but not linked yet"""
enum_list: List[Tuple[str, str, str]] = []
data_dict: Dict[str, Any] = {}
if context.selected_sequences:
strips = context.selected_sequences
else:
strips = context.scene.sequence_editor.sequences_all
# Create data dict that holds project names as key and values the corresponding sequence strips.
for strip in strips:
if strip.kitsu.linked:
project = strip.kitsu.project_name
if project not in data_dict:
data_dict[project] = []
# Append i to strips list.
if strip not in set(data_dict[project]):
data_dict[project].append(strip)
# Convert in data strucutre for enum property.
for project, strips in data_dict.items():
enum_list.append(("", project, ""))
for strip in strips:
enum_list.append((strip.name, strip.name, ""))
return enum_list
def resolve_pattern(pattern: str, var_lookup_table: Dict[str, str]) -> str:
matches = re.findall(r"\<(\w+)\>", pattern)
matches = list(set(matches))
# If no variable detected just return value.
if len(matches) == 0:
return pattern
else:
result = pattern
for to_replace in matches:
if to_replace in var_lookup_table:
to_insert = var_lookup_table[to_replace]
result = result.replace("<{}>".format(to_replace), to_insert)
else:
logger.warning(
"Failed to resolve variable: %s not defined!", to_replace
)
return ""
return result
def get_shots_enum_for_link_shot_op(
self: bpy.types.Operator, context: bpy.types.Context
) -> List[Tuple[str, str, str]]:
global _sqe_shot_enum_list
if not self.sequence_enum:
return []
zseq_active = Sequence.by_id(self.sequence_enum)
_sqe_shot_enum_list.clear()
_sqe_shot_enum_list.extend(
[(s.id, s.name, s.description or "") for s in zseq_active.get_all_shots()]
)
return _sqe_shot_enum_list
def upload_preview(
context: bpy.types.Context, filepath: Path, task_type: TaskType, comment: str = ""
) -> None:
# Get shot by id which is in filename of thumbnail.
shot_id = filepath.name.split(bkglobals.SPACE_REPLACER)[0]
shot = Shot.by_id(shot_id)
# Find task from task type for that shot, ca be None of no task was added for that task type.
task = Task.by_name(shot, task_type)
if not task:
# Turns out a entity on the server can have 0 tasks even tough task types exist
# you have to create a task first before being able to upload a thumbnail.
task_status = TaskStatus.by_short_name("wip")
task = Task.new_task(shot, task_type, task_status=task_status)
else:
task_status = TaskStatus.by_id(task.task_status_id)
# Create a comment, e.G 'Update thumbnail'.
comment_obj = task.add_comment(task_status, comment=comment)
# Add_preview_to_comment.
task.add_preview_to_comment(comment_obj, filepath.as_posix())
logger.info(f"Uploaded preview for shot: {shot.name} under: {task_type.name}")
def init_start_frame_offset(strip: bpy.types.Strip) -> None:
# Frame start offset.
offset_start = strip.frame_final_start - strip.frame_start
# Cast offset start to int, since after Blender 3.3 strip values are floats
strip.kitsu.frame_start_offset = int(offset_start)
def append_sequence_color(
context: bpy.types.Context, seq: Sequence
) -> Optional[Tuple[str, str, str]]:
"""
Extend scene.kitsu.sequence_colors property with seq.data['color'] value if it exists.
"""
# Pull sequencee color property.
if not seq.data:
logger.info("%s failed to load sequence color. Missing 'data' key")
return None
if not "color" in seq.data:
logger.info("%s failed to load sequence color. Missing data['color'] key")
return None
try:
item = context.scene.kitsu.sequence_colors[seq.id]
except:
item = context.scene.kitsu.sequence_colors.add()
item.name = seq.id
logger.info(
"Added %s to scene.kitsu.seqeuence_colors",
seq.name,
)
finally:
item.color = tuple(seq.data["color"])
return tuple(seq.data["color"])
def push_sequence_color(context: bpy.types.Context, sequence: Sequence) -> None:
# Updates sequence color and logs.
try:
item = context.scene.kitsu.sequence_colors[sequence.id]
except KeyError:
logger.info(
"%s failed to push sequence color. Does not exists in 'context.scene.kitsu.sequence_colors'",
sequence.name,
)
else:
sequence.update_data({"color": list(item.color)})
logger.info("%s pushed sequence color", sequence.name)
def create_metadata_strip(
scene: bpy.types.Scene, name: str, channel, frame_start: int, frame_end: int
) -> bpy.types.MovieStrip:
addon_prefs = prefs.addon_prefs_get(bpy.context)
strip = scene.sequence_editor.sequences.new_movie(
name,
addon_prefs.metadatastrip_file,
channel,
frame_start,
)
strip.frame_final_end = frame_end
# Set blend alpha.
strip.blend_alpha = 0
init_start_frame_offset(strip)
return strip
def link_metadata_strip(
context, shot: Shot, seq: Sequence, strip: bpy.types.MovieStrip
) -> bpy.types.MovieStrip:
# Pull shot meta.
pull.shot_meta(strip, shot)
# Rename strip.
strip.name = shot.name
# Pull sequence color.
append_sequence_color(context, seq)
return strip
+42
View File
@@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
from .. import bkglobals
from ..types import Cache, Sequence, Project, Shot
from ..logger import LoggerFactory
logger = LoggerFactory.getLogger()
def shot_meta(strip: bpy.types.Strip, shot: Shot, clear_cache: bool = True) -> None:
if clear_cache:
# Clear cache before pulling.
Cache.clear_all()
# Update sequence props.
seq = Sequence.by_id(shot.parent_id)
strip.kitsu.sequence_id = seq.id
strip.kitsu.sequence_name = seq.name
# Update shot props.
strip.kitsu.shot_id = shot.id
strip.kitsu.shot_name = shot.name
strip.kitsu.shot_description = shot.description if shot.description else ""
# Update project props.
project = Project.by_id(shot.project_id)
strip.kitsu.project_id = project.id
strip.kitsu.project_name = project.name
# Update meta props.
strip.kitsu.initialized = True
strip.kitsu.linked = True
# Update strip name.
strip.name = shot.name
# Log.
logger.info("Pulled meta from shot: %s to strip: %s", shot.name, strip.name)
+94
View File
@@ -0,0 +1,94 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Tuple
import bpy
from .. import bkglobals
from ..types import Sequence, Project, Shot
from ..logger import LoggerFactory
import gazu
logger = LoggerFactory.getLogger()
def shot_meta(strip: bpy.types.Strip, shot: Shot) -> None:
# Update shot info.
shot.name = strip.kitsu.shot_name
shot.description = strip.kitsu.shot_description
shot.data["frame_in"] = strip.frame_final_start
shot.data["frame_out"] = strip.frame_final_end
shot.data["3d_start"] = strip.kitsu_3d_start
shot.nb_frames = strip.frame_final_duration
shot.data["fps"] = bkglobals.FPS
# If user changed the sequence the shot belongs to
# (can only be done by operator not by hand).
if strip.kitsu.sequence_id != shot.sequence_id:
sequence = Sequence.by_id(strip.kitsu.sequence_id)
shot.sequence_id = sequence.id
shot.parent_id = sequence.id
shot.sequence_name = sequence.name
# Update on server.
shot.update()
logger.info("Pushed meta to shot: %s from strip: %s", shot.name, strip.name)
def new_shot(
strip: bpy.types.Strip, sequence: Sequence, project: Project, add_tasks=False
) -> Shot:
frame_range = (strip.frame_final_start, strip.frame_final_end)
shot = project.create_shot(
sequence,
strip.kitsu.shot_name,
nb_frames=strip.frame_final_duration,
frame_in=frame_range[0],
frame_out=frame_range[1],
data={"fps": bkglobals.FPS, "3d_start": bkglobals.FRAME_START},
)
if add_tasks:
create_intial_tasks(shot, project)
# Update description, no option to pass that on create.
if strip.kitsu.shot_description:
shot.description = strip.kitsu.shot_description
shot.update()
# Set project name locally, will be available on next pull.
shot.project_name = project.name
logger.info("Pushed create shot: %s for project: %s", shot.name, project.name)
return shot
def new_sequence(strip: bpy.types.Strip, project: Project) -> Sequence:
sequence = project.create_sequence(
strip.kitsu.sequence_name,
strip.kitsu.episode_id
)
logger.info(
"Pushed create sequence: %s for project: %s", sequence.name, project.name
)
return sequence
def delete_shot(strip: bpy.types.Strip, shot: Shot) -> str:
result = shot.remove()
logger.info(
"Pushed delete shot: %s for project: %s",
shot.name,
shot.project_name or "Unknown",
)
strip.kitsu.clear()
return result
def create_intial_tasks(shot: Shot, project: Project):
shot_entity = gazu.shot.get_shot(shot.id)
for task_type in gazu.task.all_task_types_for_project(project.id):
if task_type["for_entity"] == "Shot":
gazu.task.new_task(shot_entity, task_type)
+770
View File
@@ -0,0 +1,770 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
from .. import cache, prefs, ui, bkglobals
from ..sqe import checkstrip
from ..context import core as context_core
from ..logger import LoggerFactory
from ..sqe.ops import (
KITSU_OT_sqe_push_new_sequence,
KITSU_OT_sqe_push_new_shot,
KITSU_OT_sqe_push_shot_meta,
KITSU_OT_sqe_uninit_strip,
KITSU_OT_sqe_unlink_shot,
KITSU_OT_sqe_init_strip,
KITSU_OT_sqe_link_shot,
KITSU_OT_sqe_link_sequence,
KITSU_OT_sqe_set_thumbnail_task_type,
KITSU_OT_sqe_set_sqe_render_task_type,
KITSU_OT_sqe_push_render_still,
KITSU_OT_sqe_push_render,
KITSU_OT_sqe_push_del_shot,
KITSU_OT_sqe_pull_shot_meta,
KITSU_OT_sqe_multi_edit_strip,
KITSU_OT_sqe_debug_duplicates,
KITSU_OT_sqe_debug_not_linked,
KITSU_OT_sqe_debug_multi_project,
KITSU_OT_sqe_pull_edit,
KITSU_OT_sqe_init_strip_start_frame,
KITSU_OT_sqe_create_metadata_strip,
KITSU_OT_sqe_fix_metadata_strips,
KITSU_OT_sqe_add_sequence_color,
KITSU_OT_sqe_scan_for_media_updates,
KITSU_OT_sqe_change_strip_source,
KITSU_OT_sqe_clear_update_indicators,
KITSU_OT_sqe_import_image_sequence,
KITSU_OT_sqe_import_playblast,
)
from pathlib import Path
logger = LoggerFactory.getLogger()
def get_selshots_noun(nr_of_shots: int, prefix: str = "Active") -> str:
if not nr_of_shots:
noun = "All"
elif nr_of_shots == 1:
noun = f"{prefix} Shot"
else:
noun = "%i Shots" % nr_of_shots
return noun
class KITSU_MT_sqe_advanced_delete(bpy.types.Menu):
bl_label = "Advanced Delete"
def draw(self, context: bpy.types.Context) -> None:
selshots = context.selected_sequences
strips_to_unlink = [s for s in selshots if s.kitsu.linked]
layout = self.layout
layout.operator(
KITSU_OT_sqe_push_del_shot.bl_idname,
text=f"Unlink and Delete {len(strips_to_unlink)} Shots",
icon="CANCEL",
)
class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
"""
Panel in sequence editor that shows all kinds of tools related to Kitsu and sequence strips
"""
# TODO: Because each draw function was previously a seperate Panel there might be a lot of
# code duplication now, needs to be refactored at some point
bl_category = "Kitsu"
bl_label = "Shot Tools"
bl_space_type = "SEQUENCE_EDITOR"
bl_region_type = "UI"
bl_order = 20
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
if not context_core.is_edit_context():
return False
sqe = context.scene.sequence_editor
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)
if self.poll_setup(context):
self.draw_setup(context)
if self.poll_metadata(context):
self.draw_metadata(context)
if self.poll_offline_metadata(context):
self.draw_offline_metadata(context)
if self.poll_multi_edit(context):
self.draw_multi_edit(context)
if self.poll_push(context):
self.draw_push(context)
if self.poll_pull(context):
self.draw_pull(context)
self.draw_media(context)
if self.poll_debug(context):
self.draw_debug(context)
@classmethod
def poll_error(cls, context: bpy.types.Context) -> bool:
project_active = cache.project_active_get()
addon_prefs = prefs.addon_prefs_get(context)
if not prefs.session_auth(context):
return False
return bool(not project_active or not addon_prefs.is_project_root_valid)
def draw_error(self, context: bpy.types.Context) -> None:
layout = self.layout
project_active = cache.project_active_get()
box = ui.draw_error_box(layout)
addon_prefs = prefs.addon_prefs_get(context)
if not project_active:
ui.draw_error_active_project_unset(box)
if not addon_prefs.is_project_root_valid:
ui.draw_error_invalid_project_root_dir(box)
@classmethod
def poll_setup(cls, context: bpy.types.Context) -> bool:
return bool(context.selected_sequences)
def draw_setup(self, context: bpy.types.Context) -> None:
"""
Panel in SQE that shows operators to setup shots. That includes initialization,
uninitizialization, linking and unlinking.
"""
strip = context.scene.sequence_editor.active_strip
selshots = context.selected_sequences
nr_of_shots = len(selshots)
noun = get_selshots_noun(nr_of_shots)
project_active = cache.project_active_get()
strips_to_init = []
strips_to_uninit = []
strips_to_unlink = []
for s in selshots:
if s.type not in checkstrip.VALID_STRIP_TYPES:
continue
if not s.kitsu.initialized:
strips_to_init.append(s)
elif s.kitsu.linked:
strips_to_unlink.append(s)
elif s.kitsu.initialized:
strips_to_uninit.append(s)
# Create box.
layout = self.layout
box = layout.box()
box.label(text="Setup Shots", icon="TOOL_SETTINGS")
# Production.
if prefs.session_auth(context):
box.row().label(text=f"Production: {project_active.name}")
# Single Selection.
if nr_of_shots == 1:
row = box.row(align=True)
# Initialize.
if strip.type not in checkstrip.VALID_STRIP_TYPES:
row.label(text=f"Only sequence strips of types: {checkstrip.VALID_STRIP_TYPES }")
return
if not strip.kitsu.initialized:
# Init active.
row.operator(KITSU_OT_sqe_init_strip.bl_idname, text=f"Init {noun}", icon="ADD")
# Link active.
row.operator(
KITSU_OT_sqe_link_shot.bl_idname,
text=f"Link {noun}",
icon="LINKED",
)
# Create metadata strip from uninitialized strip.
row = box.row(align=True)
row.operator(
KITSU_OT_sqe_create_metadata_strip.bl_idname,
text=f"Create Metadata Strip from {noun}",
)
# Unlink.
elif strip.kitsu.linked:
row = box.row(align=True)
row.operator(
KITSU_OT_sqe_unlink_shot.bl_idname,
text=f"Unlink {noun}",
icon="UNLINKED",
)
row.menu("KITSU_MT_sqe_advanced_delete", icon="DOWNARROW_HLT", text="")
# Uninitialize.
else:
row = box.row(align=True)
# Unlink active.
row.operator(
KITSU_OT_sqe_uninit_strip.bl_idname,
text=f"Uninitialize {noun}",
icon="REMOVE",
)
# Multiple Selection.
elif nr_of_shots > 1:
row = box.row(align=True)
# Init.
if strips_to_init:
row.operator(
KITSU_OT_sqe_init_strip.bl_idname,
text=f"Init {len(strips_to_init)} Shots",
icon="ADD",
)
row = box.row(align=True)
row.operator(
KITSU_OT_sqe_create_metadata_strip.bl_idname,
text=f"Create {len(strips_to_init)} Metadata Strips",
)
# Make row.
if strips_to_uninit or strips_to_unlink:
row = box.row(align=True)
# Uninitialize.
if strips_to_uninit:
row.operator(
KITSU_OT_sqe_uninit_strip.bl_idname,
text=f"Uninitialize {len(strips_to_uninit)} Shots",
icon="REMOVE",
)
# Unlink all.
if strips_to_unlink:
row.operator(
KITSU_OT_sqe_unlink_shot.bl_idname,
text=f"Unlink {len(strips_to_unlink)} Shots",
icon="UNLINKED",
)
row.menu("KITSU_MT_sqe_advanced_delete", icon="DOWNARROW_HLT", text="")
@classmethod
def poll_metadata(cls, context: bpy.types.Context) -> bool:
nr_of_shots = len(context.selected_sequences)
strip = context.scene.sequence_editor.active_strip
if nr_of_shots == 1:
return strip.kitsu.initialized
return False
def draw_metadata(self, context: bpy.types.Context) -> None:
"""
Panel in sequence editor that shows .kitsu properties of active strip. (shot, sequence)
"""
split_factor = 0.2
strip = context.scene.sequence_editor.active_strip
# Create box.
layout = self.layout
box = layout.box()
box.label(text="Metadata", icon="ALIGN_LEFT")
col = box.column(align=True)
# Sequence.
split = col.split(factor=split_factor, align=True)
split.label(text="Sequence")
if not strip.kitsu.sequence_id:
sub_row = split.row(align=True)
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.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")
try:
sequence_color_item = context.scene.kitsu.sequence_colors[strip.kitsu.sequence_id]
except KeyError:
sub_sub_split.operator(
KITSU_OT_sqe_add_sequence_color.bl_idname, text="", icon="COLOR"
)
else:
sub_sub_split.prop(sequence_color_item, "color", text="")
# Shot.
split = col.split(factor=split_factor, align=True)
split.label(text="Shot")
if not strip.kitsu.shot_id:
placeholder = strip.kitsu.shot_name or ''
split.prop(strip.kitsu, "manual_shot_name", text="", placeholder=placeholder)
else:
split.prop(strip.kitsu, "shot_name", text="")
# Description.
split = col.split(factor=split_factor, align=True)
split.label(text="Description")
split.prop(strip.kitsu, "shot_description_display", text="")
split.enabled = False if not strip.kitsu.initialized else True
# Frame range.
split = col.split(factor=split_factor)
split.label(text="Frame Range")
row = split.row(align=False)
row.prop(strip, "kitsu_3d_start", text="In")
row.prop(strip, "kitsu_frame_end", text="Out")
row.prop(strip, "kitsu_frame_duration", text="Duration")
row.operator(KITSU_OT_sqe_init_strip_start_frame.bl_idname, text="", icon="FILE_REFRESH")
"""
split = col.split(factor=split_factor)
split.label(text="Offsets")
row = split.row(align=False)
row.prop(strip.kitsu, "frame_start_offset", text="In")
"""
def poll_offline_metadata(cls, context: bpy.types.Context) -> bool:
offline_metadata_strips = [
strip
for strip in context.scene.sequence_editor.sequences
if strip.kitsu.shot_id != '' and not Path(strip.filepath).is_file()
]
return len(offline_metadata_strips) > 0
def draw_offline_metadata(self, context: bpy.types.Context) -> None:
# Create box.
layout = self.layout
box = layout.box()
box.label(text="Fix Metadata Strips", icon="ERROR")
offline_metadata_strips = [
strip
for strip in context.selected_sequences
if strip.kitsu.shot_id != '' and not Path(strip.filepath).is_file()
]
if len(offline_metadata_strips) == 0:
text = "Fix All Missing Media"
else:
text = f"Fix {len(offline_metadata_strips)} Missing Media"
box.operator(KITSU_OT_sqe_fix_metadata_strips.bl_idname, text=text)
@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]
return bool(not unvalid and nr_of_shots > 1)
def draw_multi_edit(self, context: bpy.types.Context) -> None:
"""
Panel in sequence editor that can edit properties of multiple strips at one.
Mostly used to quickly initialize lots of shots with an increasing counter.
"""
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 Metadata", icon="PROPERTIES")
# Sequence
col = box.column()
sub_row = col.row(align=True)
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="", icon="TOOL_SETTINGS")
if context.window_manager.show_advanced:
# Counter.
box.row().prop(addon_prefs, "shot_counter_digits", text="Shot Counter Digits")
box.row().prop(addon_prefs, "shot_counter_increment", text="Shot Counter Increment")
# Variables.
row = box.row(align=True)
row.prop(
context.window_manager,
"var_use_custom_seq",
text="Custom Sequence Variable",
)
if context.window_manager.var_use_custom_seq:
row.prop(context.window_manager, "var_sequence_custom", text="")
# Project.
row = box.row(align=True)
row.prop(
context.window_manager,
"var_use_custom_project",
text="Custom Project Variable",
)
if context.window_manager.var_use_custom_project:
row.prop(context.window_manager, "var_project_custom", text="")
# Shot pattern.
box.row().prop(addon_prefs, "shot_pattern", text="Shot Pattern")
# Preview.
row = box.row()
row.prop(context.window_manager, "shot_preview", text="Preview")
row = box.row(align=True)
row.operator(
KITSU_OT_sqe_multi_edit_strip.bl_idname,
text=f"Set Metadata for {noun}",
icon="ALIGN_LEFT",
)
@classmethod
def poll_push(cls, context: bpy.types.Context) -> bool:
# If only one strip is selected and it is not init then hide panel.
if not prefs.session_auth(context):
return False
selshots = context.selected_sequences
if not selshots:
selshots = context.scene.sequence_editor.sequences_all
strips_to_meta = []
strips_to_tb = []
strips_to_submit = []
for s in selshots:
if s.kitsu.linked:
strips_to_tb.append(s)
strips_to_meta.append(s)
elif s.kitsu.initialized and (s.kitsu.manual_shot_name != "" or s.kitsu.shot_name):
strips_to_submit.append(s)
return bool(strips_to_meta or strips_to_tb or strips_to_submit)
def draw_push(self, context: bpy.types.Context) -> None:
"""
Panel that shows operator to sync sequence editor metadata with backend.
"""
nr_of_shots = len(context.selected_sequences)
layout = self.layout
strip = context.scene.sequence_editor.active_strip
selshots = context.selected_sequences
if not selshots:
selshots = context.scene.sequence_editor.sequences_all
strips_to_meta = []
strips_to_tb = []
strips_to_submit = []
strips_to_delete = []
for s in selshots:
if s.kitsu.linked:
strips_to_tb.append(s)
strips_to_meta.append(s)
strips_to_delete.append(s)
elif s.kitsu.initialized:
if s.kitsu.shot_name and s.kitsu.sequence_name:
strips_to_submit.append(s)
# Create box.
layout = self.layout
box = layout.box()
box.label(text="Push", icon="EXPORT")
# Special case if one shot is selected and it is init but not linked
# shows the operator but it is not enabled until user types in required metadata.
if nr_of_shots == 1 and not strip.kitsu.linked:
# New operator.
row = box.row()
col = row.column(align=True)
col.operator(
KITSU_OT_sqe_push_new_shot.bl_idname,
text="Submit New Shot",
icon="ADD",
)
return
# Either way no selection one selection but linked or multiple.
# Metadata operator.
row = box.row()
if strips_to_meta:
col = row.column(align=True)
noun = get_selshots_noun(len(strips_to_meta), prefix=f"{len(strips_to_meta)}")
col.operator(
KITSU_OT_sqe_push_shot_meta.bl_idname,
text=f"Metadata {noun}",
icon="ALIGN_LEFT",
)
# Thumbnail and seqeunce renderoperator.
if strips_to_tb:
# Upload thumbnail op.
noun = get_selshots_noun(len(strips_to_tb), prefix=f"{len(strips_to_meta)}")
split = col.split(factor=0.7, align=True)
split.operator(
KITSU_OT_sqe_push_render_still.bl_idname,
text=f"Render Still {noun}",
icon="IMAGE_DATA",
)
# Select task types op.
noun = context.scene.kitsu.task_type_thumbnail_name or "Select Task Type"
split.operator(
KITSU_OT_sqe_set_thumbnail_task_type.bl_idname,
text=noun,
icon="DOWNARROW_HLT",
)
# Sqe render op.
noun = get_selshots_noun(len(strips_to_tb), prefix=f"{len(strips_to_meta)}")
split = col.split(factor=0.7, align=True)
split.operator(
KITSU_OT_sqe_push_render.bl_idname,
text=f"Render Movie {noun}",
icon="IMAGE_DATA",
)
# Select task types op.
noun = context.scene.kitsu.task_type_sqe_render_name or "Select Task Type"
split.operator(
KITSU_OT_sqe_set_sqe_render_task_type.bl_idname,
text=noun,
icon="DOWNARROW_HLT",
)
# Submit operator.
if nr_of_shots > 0:
if strips_to_submit:
noun = get_selshots_noun(len(strips_to_submit), prefix=f"{len(strips_to_submit)}")
row = box.row()
col = row.column(align=True)
col.operator(
KITSU_OT_sqe_push_new_shot.bl_idname,
text=f"Submit {noun}",
icon="ADD",
)
@classmethod
def poll_pull(cls, context: bpy.types.Context) -> bool:
if not prefs.session_auth(context):
return False
selshots = context.selected_sequences
all_shots = context.scene.sequence_editor.sequences_all
if not selshots: # Pull entire edit.
return True
strips_to_meta_sel = [s for s in selshots if s.kitsu.linked]
strips_to_meta_all = [s for s in all_shots if s.kitsu.linked]
if not selshots:
return bool(strips_to_meta_all)
return bool(strips_to_meta_sel)
def draw_pull(self, context: bpy.types.Context) -> None:
"""
Panel that shows operator to sync sequence editor metadata with backend.
"""
selshots = context.selected_sequences
if not selshots:
selshots = context.scene.sequence_editor.sequences_all
strips_to_meta = []
for s in selshots:
if s.kitsu.linked:
strips_to_meta.append(s)
# Create box.
layout = self.layout
box = layout.box()
box.label(text="Pull", icon="IMPORT")
layout = self.layout
if strips_to_meta:
noun = get_selshots_noun(len(strips_to_meta), prefix=f"{len(strips_to_meta)}")
row = box.row()
row.operator(
KITSU_OT_sqe_pull_shot_meta.bl_idname,
text=f"Metadata {noun}",
icon="ALIGN_LEFT",
)
if not context.selected_sequences:
row = box.row()
row.operator(
KITSU_OT_sqe_pull_edit.bl_idname,
text=f"Pull entire Edit",
icon="FILE_MOVIE",
)
@classmethod
def poll_debug(cls, context: bpy.types.Context) -> bool:
return prefs.addon_prefs_get(context).enable_debug
def draw_debug(self, context: bpy.types.Context) -> None:
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="Debug", icon="MODIFIER_ON")
row = box.row()
row.operator(
KITSU_OT_sqe_debug_duplicates.bl_idname,
text=f"Duplicates {noun}",
icon="MODIFIER_ON",
)
row = box.row()
row.operator(
KITSU_OT_sqe_debug_not_linked.bl_idname,
text=f"Not Linked {noun}",
icon="MODIFIER_ON",
)
row = box.row()
row.operator(
KITSU_OT_sqe_debug_multi_project.bl_idname,
text=f"Multi Projects {noun}",
icon="MODIFIER_ON",
)
def draw_media(self, context: bpy.types.Context) -> None:
sel_metadata_strips = [strip for strip in context.selected_sequences if strip.kitsu.linked]
noun = get_selshots_noun(len(sel_metadata_strips), prefix=f"{len(sel_metadata_strips)}")
playblast = "Playblast" if len(sel_metadata_strips) <= 1 else "Playblasts"
sequence = "Sequence" if len(sel_metadata_strips) <= 1 else "Sequences"
# Create box.
layout = self.layout
box = layout.box()
box.label(text="Media", icon="RENDER_ANIMATION")
box.operator(
KITSU_OT_sqe_import_playblast.bl_idname,
text=f"Import {noun} {playblast}",
icon="FILE_MOVIE",
)
box.operator(
KITSU_OT_sqe_import_image_sequence.bl_idname,
text=f"Import {noun} Image {sequence}",
icon="RENDER_RESULT",
)
class KITSU_PT_sqe_general_tools(bpy.types.Panel):
"""
Panel in sequence editor that shows tools that don't relate directly to Kitsu
"""
bl_category = "Kitsu"
bl_label = "General Tools"
bl_space_type = "SEQUENCE_EDITOR"
bl_region_type = "UI"
bl_order = 30
bl_options = {"DEFAULT_CLOSED"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
if not context_core.is_edit_context():
return False
selshots = context.selected_sequences
sqe = context.scene.sequence_editor
if not sqe:
return False
if not selshots:
selshots = context.scene.sequence_editor.sequences_all
movie_strips = [s for s in selshots if s.type == "MOVIE"]
return bool(movie_strips)
def draw(self, context: bpy.types.Context) -> None:
active_strip = context.scene.sequence_editor.active_strip
selshots = context.selected_sequences
if not selshots:
selshots = context.scene.sequence_editor.sequences_all
strips_to_update_media = []
for s in selshots:
if s.type == "MOVIE":
strips_to_update_media.append(s)
# Create box.
layout = self.layout
box = layout.box()
box.label(text="General", icon="MODIFIER")
# Scan for outdated media and reset operator.
row = box.row(align=True)
row.operator(
KITSU_OT_sqe_scan_for_media_updates.bl_idname,
text=f"Check media update for {len(strips_to_update_media)} {'strip' if len(strips_to_update_media) == 1 else 'strips'}",
)
row.operator(KITSU_OT_sqe_clear_update_indicators.bl_idname, text="", icon="X")
# Up down source operator. Check for to strips to accommodate linked strips
if len(selshots) <= 2 and active_strip and active_strip.type == "MOVIE":
row = box.row(align=True)
row.prop(active_strip, "filepath_display", text="")
row.operator(
KITSU_OT_sqe_change_strip_source.bl_idname, text="", icon="TRIA_UP"
).direction = "UP"
row.operator(
KITSU_OT_sqe_change_strip_source.bl_idname, text="", icon="TRIA_DOWN"
).direction = "DOWN"
row.operator(
KITSU_OT_sqe_change_strip_source.bl_idname, text="", icon="FILE_PARENT"
).go_latest = True
# ---------REGISTER ----------.
classes = [
KITSU_MT_sqe_advanced_delete,
KITSU_PT_sqe_shot_tools,
KITSU_PT_sqe_general_tools,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)