import textwrap import bpy from .. import config, tools_ops from ..core import CheckContext, Severity, checks_for_context, contexts, runner from ..core.runner import Status 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_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_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 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_sanity_sequencer(_SanityPanel, bpy.types.Panel): bl_idname = "WTD_PT_sanity_sequencer" bl_space_type = "SEQUENCE_EDITOR" classes = (WTD_PT_sanity_view3d, WTD_PT_tools_view3d, 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)