import textwrap import bpy from .. import config, rename, shot_viewer, tools_ops from ..core import CheckContext, Severity, checks_for_context, contexts, runner from ..core.runner import Status from ..scene_manager import ipc from ..scene_manager import library as sm_library from ..scene_manager import operators as sm from .operators import EXPANDED STATUS_ICONS = { Status.NOT_RUN: "DOT", Status.PASSED: "CHECKMARK", Status.INFO: "INFO", Status.WARNING: "ERROR", Status.ERROR: "CANCEL", Status.SKIPPED: "REMOVE", Status.CRASHED: "GHOST_ENABLED", } SEVERITY_ICONS = { Severity.INFO: "INFO", Severity.WARNING: "ERROR", Severity.ERROR: "CANCEL", } def _wrapped_label(layout, context, text, icon="NONE", margin=0): """Label wrapped to the region width. ``margin`` is the room (in characters) taken by buttons.""" char_width = 6.5 * context.preferences.system.ui_scale width = max(16, int(context.region.width / char_width) - 6 - margin) col = layout.column(align=True) for index, line in enumerate(textwrap.wrap(text, width) or [""]): col.label(text=line, icon=icon if index == 0 else "BLANK1") def draw_sanity_checks_panel(self, context): layout = self.layout scene = context.scene settings = scene.wtd_sanity report = runner.LAST_REPORT if report is not None and report.context_id != settings.context: report = None layout.prop(settings, "context", text="") if settings.context in contexts.ASSET: row = layout.row(align=True) row.prop(settings, "asset_name", text="Asset") if not settings.asset_name: guessed = CheckContext(scene, settings.context).asset_name row.label(text=guessed or "(from file name)") row = layout.row(align=True) row.scale_y = 1.4 row.operator("wtd_sanity.run_checks", text="Run Checks", icon="PLAY") row.operator("wtd_sanity.fix_all", text="Fix All", icon="TOOL_SETTINGS") row.operator("wtd_sanity.write_report", text="", icon="TEXT") if settings.context == contexts.CHAR_MOD: layout.operator("wtd_sanity.export_char_reference", icon="EXPORT") if report is not None: counts = report.counts() row = layout.row() for status in ( Status.PASSED, Status.WARNING, Status.ERROR, Status.SKIPPED, Status.CRASHED, ): if counts[status]: row.label(text=str(counts[status]), icon=STATUS_ICONS[status]) disabled = runner.disabled_check_ids(scene) box = None category = None for check in checks_for_context(settings.context): if check.category != category: category = check.category box = layout.box() box.label(text=category) _draw_check(box, context, check, report, check.id in disabled) def _draw_check(layout, context, check, report, disabled): result = report.results.get(check.id) if report else None status = result.status if result else Status.NOT_RUN expanded = check.id in EXPANDED row = layout.row(align=True) row.operator( "wtd_sanity.toggle_check", text="", emboss=False, icon="CHECKBOX_DEHLT" if disabled else "CHECKBOX_HLT", ).check_id = check.id row.operator( "wtd_sanity.toggle_expand", text="", emboss=False, icon="DOWNARROW_HLT" if expanded else "RIGHTARROW", ).check_id = check.id label_row = row.row(align=True) label_row.enabled = not disabled label_row.alert = status in {Status.ERROR, Status.CRASHED} count = f" ({len(result.issues)})" if result and result.issues else "" label_row.label(text=check.label + count, icon=STATUS_ICONS[status]) buttons = row.row(align=True) if result and any(issue.id_name or issue.elements for issue in result.issues): buttons.operator( "wtd_sanity.select_issues", text="", icon="RESTRICT_SELECT_OFF" ).check_id = check.id if result and result.failed and check.can_fix(): buttons.operator( "wtd_sanity.fix_check", text="", icon="TOOL_SETTINGS" ).check_id = check.id buttons.operator( "wtd_sanity.run_checks", text="", icon="FILE_REFRESH" ).check_id = check.id if not expanded: return col = layout.column() col.separator(factor=0.3) _wrapped_label(col, context, check.description, icon="QUESTION") if result is None: return if result.message: _wrapped_label( col, context, result.message.strip().splitlines()[-1], icon=STATUS_ICONS[status], ) for index, issue in enumerate(result.issues[: config.MAX_ISSUES_DRAWN]): severity = issue.severity or check.severity issue_row = col.row(align=True) selectable = bool(issue.id_name or issue.elements) _wrapped_label( issue_row, context, issue.message, icon=SEVERITY_ICONS[severity], margin=7 if selectable else 0, ) if selectable: op = issue_row.operator( "wtd_sanity.select_issues", text="", icon="RESTRICT_SELECT_OFF", emboss=False, ) op.check_id = check.id op.issue_index = index if check.editable_names: id_block = issue.get_id() if id_block is not None and id_block.library is None: col.prop(id_block, "name", text="", icon="BLANK1") hidden = len(result.issues) - config.MAX_ISSUES_DRAWN if hidden > 0: col.label(text=f"... {hidden} more (see Write Report)", icon="BLANK1") col.separator(factor=0.5) class _SanityPanel: bl_region_type = "UI" bl_category = "WTD" bl_label = "Sanity Check" bl_options = {"DEFAULT_CLOSED"} # panel starts collapsed def draw(self, context): draw_sanity_checks_panel(self, context) class _ToolsPanel: bl_region_type = "UI" bl_category = "WTD" bl_label = "Tools" def draw(self, context) -> None: layout = self.layout layout.operator( tools_ops.WTDGetOidForPath.bl_idname, text=tools_ops.WTDGetOidForPath.bl_label, ) # Adds a separator line layout.separator() class _RenamePanel: bl_region_type = "UI" bl_category = "WTD" bl_label = "Rename" bl_options = {"DEFAULT_CLOSED"} # panel starts collapsed PREVIEW_COUNT = 10 def draw(self, context) -> None: layout = self.layout settings = context.scene.wtd_rename layout.row().prop(settings, "target", expand=True) col = layout.column() col.use_property_split = True col.use_property_decorate = False col.prop(settings, "base_name") col.prop(settings, "prefix_mode") if settings.prefix_mode == "CUSTOM": col.prop(settings, "prefix_custom", text=" ") col.prop(settings, "side") col.prop(settings, "suffix_mode") if settings.suffix_mode == "CUSTOM": col.prop(settings, "suffix_custom", text=" ") col.prop(settings, "sort") if settings.target == "OBJECTS": col.prop(settings, "rename_data") plan, skipped = rename.plan_renames(context, settings) row = layout.row() row.scale_y = 1.4 row.operator( rename.WTD_OT_batch_rename.bl_idname, text=f"Rename {len(plan)} Item(s)" if plan else "Rename", icon="SORTALPHA", ) if not plan and not skipped: return box = layout.box() col = box.column(align=True) for id_block, name in plan[: self.PREVIEW_COUNT]: col.label(text=f"{id_block.name} → {name}") hidden = len(plan) - self.PREVIEW_COUNT if hidden > 0: col.label(text=f"... {hidden} more") for name in skipped: col.label(text=f"{name}: no base name, set a name", icon="ERROR") class WTD_UL_shots(bpy.types.UIList): """Camera markers of the scene, in time order.""" def draw_item( self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0, ): start, end = shot_viewer.shot_range(item) split = layout.split(factor=0.4) split.label(text=item.name, icon="MARKER_HLT") split = split.split(factor=0.6) split.label(text=item.camera.name) split.label(text=f"{start}-{end}") def filter_items(self, context, data, propname): markers = getattr(data, propname) ordered = [marker.as_pointer() for marker in shot_viewer.shots(data)] position = {pointer: index for index, pointer in enumerate(ordered)} flags = [ self.bitflag_filter_item if marker.as_pointer() in position else 0 for marker in markers ] if self.filter_name: by_name = bpy.types.UI_UL_list.filter_items_by_name( self.filter_name, self.bitflag_filter_item, markers, "name" ) flags = [flag & name_flag for flag, name_flag in zip(flags, by_name)] # Shots in time order, the markers without camera after them. order = [] hidden = len(ordered) for marker in markers: pointer = marker.as_pointer() if pointer in position: order.append(position[pointer]) else: order.append(hidden) hidden += 1 return flags, order class _ShotViewerPanel: bl_region_type = "UI" bl_category = "WTD" bl_label = "Shot Manager" bl_options = {"DEFAULT_CLOSED"} # panel starts collapsed def draw(self, context) -> None: layout = self.layout settings = context.window_manager.wtd_shot_viewer scene = context.scene has_shots = bool(shot_viewer.shots(scene)) row = layout.row(align=True) row.prop(scene, "wtd_sequence") row.label(text=shot_viewer.sequence_name(scene)) row = layout.row(align=True) row.scale_y = 1.4 row.enabled = has_shots row.operator( shot_viewer.WTD_OT_shot_viewer_step.bl_idname, text="Previous", icon="TRIA_LEFT", ).step = -1 row.operator( shot_viewer.WTD_OT_shot_viewer_step.bl_idname, text="Next", icon="TRIA_RIGHT", ).step = 1 row = layout.row() row.template_list( "WTD_UL_shots", "", scene, "timeline_markers", settings, "shot_index", rows=5, ) col = row.column(align=True) col.operator( shot_viewer.WTD_OT_shot_viewer_new_shot.bl_idname, text="", icon="ADD" ) col.operator( shot_viewer.WTD_OT_shot_viewer_delete_shot.bl_idname, text="", icon="REMOVE" ) if not has_shots: layout.label(text="No camera marker yet: add a shot with +", icon="INFO") layout.prop(settings, "loop_shot") groups = shot_viewer.rig_groups(context.view_layer) if not groups: layout.label(text="No rig in the scene", icon="INFO") for label, rigs in groups.items(): box = layout.box() box.label(text=label, icon="ARMATURE_DATA") col = box.column(align=True) for rig in rigs: col.operator( shot_viewer.WTD_OT_shot_viewer_select_rig.bl_idname, text=rig.name, depress=rig.select_get(), ).name = rig.name MESSAGE_ICONS = {"INFO": "INFO", "WARNING": "ERROR", "ERROR": "CANCEL"} class _SceneManagerPanel: bl_region_type = "UI" bl_category = "WTD" bl_label = "Scene Manager" def draw(self, context) -> None: layout = self.layout state = sm.STATE scene = context.scene row = layout.row(align=True) if sm.connected(): session = sm.current_session() text = f"{session.get('session_name', '?')} ({session.get('cluster_name', '')})" if state.ping.get("user"): text += f" {state.ping['user']} @ {state.ping.get('site', '')}" row.label(text=text, icon="LINKED") else: row.label(text=state.connection_error, icon="UNLINKED") row.operator(sm.WTD_OT_scene_connect.bl_idname, text="", icon="FILE_REFRESH") if len(state.sessions) > 1: col = layout.column(align=True) chosen = context.window_manager.wtd_libreflow.session_uid for uid, info in state.sessions.items(): col.operator( sm.WTD_OT_scene_select_session.bl_idname, text=f"{info.get('session_name', uid)}: {info.get('current_oid') or ''}", depress=uid == chosen, ).uid = uid layout.prop(scene.wtd_scene_manager, "context_oid", text="", icon="SEQUENCE") row = layout.row(align=True) row.operator( sm.WTD_OT_scene_use_current_oid.bl_idname, text="Libreflow Oid", icon="IMPORT", ) row.operator( sm.WTD_OT_scene_pick_shot.bl_idname, text="Pick Shot", icon="VIEWZOOM" ) text, valid = sm.context_label(sm.context_oid(scene)) if text: row = layout.row() row.alert = not valid _wrapped_label(row, context, text, icon="CHECKMARK" if valid else "ERROR") busy = ipc.busy_label() if busy: layout.label(text=f"{busy}...", icon="SORTTIME") message, level = state.message if message: row = layout.row() row.alert = level == "ERROR" _wrapped_label(row, context, message, icon=MESSAGE_ICONS[level]) class _SceneCastingPanel: bl_region_type = "UI" bl_category = "WTD" bl_label = "Casting" bl_parent_id = "WTD_PT_scene_manager_view3d" def draw(self, context) -> None: layout = self.layout oid = sm.context_oid(context.scene) row = layout.row(align=True) row.scale_y = 1.3 row.operator(sm.WTD_OT_scene_load.bl_idname, text="Load Missing", icon="IMPORT") if sm.missing_files(context.scene): row.operator( sm.WTD_OT_scene_request.bl_idname, text="Request Missing", icon="URL" ) row.operator(sm.WTD_OT_scene_refresh.bl_idname, text="", icon="FILE_REFRESH") casting = sm.STATE.casting.get(oid) if casting is None: text = ( "Not read yet: refresh" if sm.is_shot_context(oid) else "Set a shot as context" ) layout.label(text=text, icon="INFO") return if not casting["assets"]: layout.label(text="Nothing cast in this shot", icon="INFO") found = sm_library.all_instances() col = layout.column(align=True) for asset in casting["assets"]: library = ( sm_library.find_library(asset["file"]["file_oid"]) if asset.get("file") else None ) have = len(found.get(library.name, [])) if library else 0 want = asset.get("nb_occurences") or 1 error = asset.get("error") file = ( sm.file_data(asset["file"]["file_oid"], asset["file"]) if asset.get("file") else None ) latest = (file or {}).get("latest") or {} requested = bool(error) and latest.get("sync_status") == "Requested" if requested: error = f"{latest['name']} requested, waiting for the download" icon = ( "SORTTIME" if requested else "ERROR" if error else "CHECKMARK" if have >= want else "IMPORT" ) row = col.row(align=True) row.alert = bool(error) and not requested row.label( text=f"{asset['name']} ({asset.get('asset_type', '')})", icon=icon ) row.label(text=f"{have}/{want}") if not error and have < want: row.operator( sm.WTD_OT_scene_load.bl_idname, text="", icon="IMPORT" ).asset_oid = asset["oid"] if sm.to_request(file): row.operator( sm.WTD_OT_scene_request.bl_idname, text="", icon="URL" ).file_oid = file["file_oid"] if error: sub = col.row() sub.alert = not requested _wrapped_label(sub, context, error, icon="BLANK1") cast = {asset["oid"] for asset in casting["assets"]} extra = [ library for library in sm_library.managed_libraries() if library["wtd_asset_oid"] not in cast ] if extra: box = layout.box() box.label(text="Loaded, no longer cast:", icon="ERROR") for library in extra: box.label( text=library.get("wtd_asset_name", library.name), icon="BLANK1" ) class _SceneLoadedPanel: bl_region_type = "UI" bl_category = "WTD" bl_label = "Loaded" bl_parent_id = "WTD_PT_scene_manager_view3d" def draw(self, context) -> None: layout = self.layout layout.operator( sm.WTD_OT_scene_browse.bl_idname, text="Load...", icon="VIEWZOOM" ) libraries = sm_library.managed_libraries() if not libraries: layout.label(text="Nothing loaded by the Scene Manager", icon="INFO") return row = layout.row(align=True) row.operator( sm.WTD_OT_scene_update.bl_idname, text="Update All", icon="FILE_REFRESH" ) row.operator(sm.WTD_OT_scene_refresh.bl_idname, text="", icon="VIEWZOOM") found = sm_library.all_instances() for library in libraries: state, text = sm.version_status(library) outdated = state in {"UPDATE", "PENDING"} expanded = library.name in sm.EXPANDED row = layout.row(align=True) row.operator( sm.WTD_OT_scene_toggle_expand.bl_idname, text="", emboss=False, icon="DOWNARROW_HLT" if expanded else "RIGHTARROW", ).library = library.name row.label( text=library.get("wtd_asset_name", library.name), icon="LIBRARY_DATA_DIRECT", ) version = row.row(align=True) version.alert = outdated version.label(text=library.get("wtd_revision", "?")) if state == "UPDATE": row.operator( sm.WTD_OT_scene_update.bl_idname, text="", icon="FILE_REFRESH" ).library = library.name elif state == "PENDING" and sm.to_request( sm.file_data(library["wtd_file_oid"]) ): row.operator( sm.WTD_OT_scene_request.bl_idname, text="", icon="URL" ).file_oid = library["wtd_file_oid"] row.operator( sm.WTD_OT_scene_pick_revision.bl_idname, text="", icon="COLLAPSEMENU" ).library = library.name row.operator( sm.WTD_OT_scene_remove.bl_idname, text="", icon="X" ).library = library.name if outdated: sub = layout.row() sub.alert = True _wrapped_label(sub, context, text, icon="BLANK1") if not expanded: continue col = layout.column(align=True) for instance in found.get(library.name, []): is_object = isinstance(instance, bpy.types.Object) sub = col.row(align=True) sub.label( text=instance.name, icon="OUTLINER_OB_EMPTY" if is_object else "OUTLINER_COLLECTION", ) op = sub.operator( sm.WTD_OT_scene_select_instance.bl_idname, text="", icon="RESTRICT_SELECT_OFF", ) op.library, op.instance = library.name, instance.name sub.operator( sm.WTD_OT_scene_remove.bl_idname, text="", icon="X" ).instance = instance.name lods = sm_library.lods(library, instance) if lods: current = sm_library.current_lod(library, instance) sub = col.row(align=True) sub.label(text="LOD", icon="BLANK1") for name in lods: op = sub.operator( sm.WTD_OT_scene_set_lod.bl_idname, text=name, depress=name == current, ) op.lod, op.library, op.instance = ( name, library.name, instance.name, ) layout.separator() row = layout.row(align=True) row.operator( sm.WTD_OT_scene_update.bl_idname, text="Update Selected" ).selected = True row.operator( sm.WTD_OT_scene_remove.bl_idname, text="Remove Selected" ).selected = True selected = [ (library, instance) for library, instance in sm_library.selected_instances(context.view_layer) if sm_library.lods(library, instance) ] if selected: col = layout.column(align=True) col.label(text="LOD of Selected") row = col.row(align=True) shown = { sm_library.current_lod(library, instance) for library, instance in selected } names = { name for library, instance in selected for name in sm_library.lods(library, instance) } for name in sorted(names, key=sm_library.lod_order): op = row.operator( sm.WTD_OT_scene_set_lod.bl_idname, text=name, depress=shown == {name}, ) op.lod, op.selected = name, True class _SceneAssetizePanel: bl_region_type = "UI" bl_category = "WTD" bl_label = "Assetize" bl_parent_id = "WTD_PT_scene_manager_view3d" bl_options = {"DEFAULT_CLOSED"} def draw(self, context) -> None: layout = self.layout _wrapped_label( layout, context, "Publish a local collection of this scene to Libreflow as an asset.", icon="INFO", ) layout.operator( sm.WTD_OT_scene_assetize.bl_idname, text="Assetize Collection...", icon="EXPORT", ) class WTD_PT_sanity_view3d(_SanityPanel, bpy.types.Panel): bl_idname = "WTD_PT_sanity_view3d" bl_space_type = "VIEW_3D" class WTD_PT_tools_view3d(_ToolsPanel, bpy.types.Panel): bl_idname = "WTD_PT_tools_view3d" bl_space_type = "VIEW_3D" class WTD_PT_rename_view3d(_RenamePanel, bpy.types.Panel): bl_idname = "WTD_PT_rename_view3d" bl_space_type = "VIEW_3D" class WTD_PT_rename_node_editor(_RenamePanel, bpy.types.Panel): bl_idname = "WTD_PT_rename_node_editor" bl_space_type = "NODE_EDITOR" class WTD_PT_shot_viewer_view3d(_ShotViewerPanel, bpy.types.Panel): bl_idname = "WTD_PT_shot_viewer_view3d" bl_space_type = "VIEW_3D" class WTD_PT_scene_manager_view3d(_SceneManagerPanel, bpy.types.Panel): bl_idname = "WTD_PT_scene_manager_view3d" bl_space_type = "VIEW_3D" class WTD_PT_scene_casting_view3d(_SceneCastingPanel, bpy.types.Panel): bl_idname = "WTD_PT_scene_casting_view3d" bl_space_type = "VIEW_3D" class WTD_PT_scene_loaded_view3d(_SceneLoadedPanel, bpy.types.Panel): bl_idname = "WTD_PT_scene_loaded_view3d" bl_space_type = "VIEW_3D" class WTD_PT_scene_assetize_view3d(_SceneAssetizePanel, bpy.types.Panel): bl_idname = "WTD_PT_scene_assetize_view3d" bl_space_type = "VIEW_3D" class WTD_PT_sanity_sequencer(_SanityPanel, bpy.types.Panel): bl_idname = "WTD_PT_sanity_sequencer" bl_space_type = "SEQUENCE_EDITOR" classes = ( WTD_UL_shots, WTD_PT_sanity_view3d, WTD_PT_tools_view3d, WTD_PT_shot_viewer_view3d, WTD_PT_scene_manager_view3d, WTD_PT_scene_casting_view3d, WTD_PT_scene_loaded_view3d, WTD_PT_scene_assetize_view3d, WTD_PT_rename_view3d, WTD_PT_rename_node_editor, WTD_PT_sanity_sequencer, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)