From 0cee6163aa83ec7665e06933570f71f5ebcb641d Mon Sep 17 00:00:00 2001 From: pullusb Date: Tue, 3 Dec 2024 14:26:43 +0100 Subject: [PATCH] Improve file checker 4.0.2 changed: File checker doest not fix directly when clicked: - List potential change and display an Apply fix changed: Enhanced visibility conflict list: - Also include viewlayer hide value - Allow to set all hide value from the state of one of the three --- CHANGELOG.md | 9 ++ OP_file_checker.py | 235 +++++++++++++++++++++++++++++++++------------ __init__.py | 6 +- properties.py | 4 - utils.py | 36 ++++--- 5 files changed, 208 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5912f61..4b0b139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +4.0.2 + +changed: File checker doest not fix directly when clicked: + - List potential change and display an Apply fix +changed: Enhanced visibility conflict list: + - Also include viewlayer hide value + - Allow to set all hide value from the state of one of the three +- fixed: material move operator + 4.0.1 - fixed: layer nav operator on page up/down diff --git a/OP_file_checker.py b/OP_file_checker.py index 18d25a3..c7d873b 100755 --- a/OP_file_checker.py +++ b/OP_file_checker.py @@ -4,6 +4,11 @@ from pathlib import Path import numpy as np from . import utils +from bpy.props import (BoolProperty, + PointerProperty, + CollectionProperty, + StringProperty) + def remove_stroke_exact_duplications(apply=True): '''Remove accidental stroke duplication (points exactly in the same place) :apply: Remove the duplication instead of just listing dupes @@ -53,6 +58,10 @@ class GPTB_OT_file_checker(bpy.types.Operator): # Disable use light on all object # Remove redundant strokes in frames + apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False, + description="Apply possible fixes instead of just listing (pop the list again in fix mode)", + options={'SKIP_SAVE'}) + def invoke(self, context, event): # need some self-control (I had to...) self.ctrl = event.ctrl @@ -63,10 +72,14 @@ class GPTB_OT_file_checker(bpy.types.Operator): fix = prefs.fixprops problems = [] - apply = not fix.check_only + ## Old method : Apply fixes based on pref (inverted by ctrl key) + # apply = not fix.check_only + # # If Ctrl is pressed, invert behavior (invert boolean) + # apply ^= self.ctrl - # If Ctrl is pressed, invert behavior (invert boolean) - apply ^= self.ctrl + apply = self.apply_fixes + if self.ctrl: + apply = True ## Lock main cam: if fix.lock_main_cam: @@ -169,13 +182,14 @@ class GPTB_OT_file_checker(bpy.types.Operator): if fix.list_obj_vis_conflict: viz_ct = 0 for o in context.scene.objects: - if o.hide_viewport != o.hide_render: + if not (o.hide_get() == o.hide_viewport == o.hide_render): + hv = 'No' if o.hide_get() else 'Yes' vp = 'No' if o.hide_viewport else 'Yes' rd = 'No' if o.hide_render else 'Yes' viz_ct += 1 - print(f'{o.name} : viewport {vp} != render {rd}') + print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}') if viz_ct: - problems.append(['gp.list_object_visibility', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE']) + problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE']) ## GP modifiers visibility conflict if fix.list_gp_mod_vis_conflict: @@ -280,9 +294,13 @@ class GPTB_OT_file_checker(bpy.types.Operator): print(p) else: print(p[0]) - + + if not self.apply_fixes: + ## button to call the operator again with apply_fixes set to True + problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}]) + # Show in viewport - title = "Changed Settings" if apply else "Checked Settings (dry run, nothing changed)" + title = "Changed Settings" if apply else "Checked Settings (nothing changed)" utils.show_message_box(problems, _title = title, _icon = 'INFO') else: self.report({'INFO'}, 'All good') @@ -482,48 +500,13 @@ class GPTB_OT_links_checker(bpy.types.Operator): return context.window_manager.invoke_props_dialog(self, width=popup_width) -""" OLD links checker with show_message_box -class GPTB_OT_links_checker(bpy.types.Operator): - bl_idname = "gp.links_checker" - bl_label = "Links check" - bl_description = "Check states of file direct links" - bl_options = {"REGISTER"} - - def execute(self, context): - all_lnks = [] - has_broken_link = False - ## check for broken links - for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)): - lfp = Path(lib) - realib = Path(current) - if not lfp.exists(): - has_broken_link = True - all_lnks.append( (f"Broken link: {realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix() - else: - if realib.as_posix().startswith('//'): - all_lnks.append( (f"Link: {realib.as_posix()}", 'LINKED') )#lfp.as_posix() - else: - all_lnks.append( (f"Link: {realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix() - all_lnks.sort(key=lambda x: x[1], reverse=True) - if all_lnks: - print('===File check===') - for p in all_lnks: - if isinstance(p, str): - print(p) - else: - print(p[0]) - # Show in viewport - utils.show_message_box(all_lnks, _title = "Links", _icon = 'INFO') - return {"FINISHED"} """ - - -class GPTB_OT_list_object_visibility(bpy.types.Operator): - bl_idname = "gp.list_object_visibility" - bl_label = "List Object Visibility Conflicts" +class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator): + bl_idname = "gp.list_viewport_render_visibility" + bl_label = "List Viewport And Render Visibility Conflicts" bl_description = "List objects visibility conflicts, when viewport and render have different values" bl_options = {"REGISTER"} - + def invoke(self, context, event): self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render] return context.window_manager.invoke_props_dialog(self, width=250) @@ -539,20 +522,142 @@ class GPTB_OT_list_object_visibility(bpy.types.Operator): def execute(self, context): return {'FINISHED'} - ## basic listing as message box # all in invoke now - # li = [] - # viz_ct = 0 - # for o in context.scene.objects: - # if o.hide_viewport != o.hide_render: - # vp = 'No' if o.hide_viewport else 'Yes' - # rd = 'No' if o.hide_render else 'Yes' - # viz_ct += 1 - # li.append(f'{o.name} : viewport {vp} != render {rd}') - # if li: - # utils.show_message_box(_message=li, _title=f'{viz_ct} visibility conflicts found') - # else: - # self.report({'INFO'}, f"No Object visibility conflict on current scene") - # return {'FINISHED'} + +### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu) +class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator): + bl_idname = "gp.sync_visibility_from_viewlayer" + bl_label = "Sync Visibility From Viewlayer" + bl_description = "Set viewport and render visibility to match viewlayer visibility" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + for obj in context.scene.objects: + is_hidden = obj.hide_get() # Get viewlayer visibility + obj.hide_viewport = is_hidden + obj.hide_render = is_hidden + return {'FINISHED'} + +class GPTB_OT_sync_visibility_from_viewport(bpy.types.Operator): + bl_idname = "gp.sync_visibility_from_viewport" + bl_label = "Sync Visibility From Viewport" + bl_description = "Set viewlayer and render visibility to match viewport visibility" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + for obj in context.scene.objects: + is_hidden = obj.hide_viewport + obj.hide_set(is_hidden) + obj.hide_render = is_hidden + return {'FINISHED'} + +class GPTB_OT_sync_visibility_from_render(bpy.types.Operator): + bl_idname = "gp.sync_visibility_from_render" + bl_label = "Sync Visibility From Render" + bl_description = "Set viewlayer and viewport visibility to match render visibility" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + for obj in context.scene.objects: + is_hidden = obj.hide_render + obj.hide_set(is_hidden) + obj.hide_viewport = is_hidden + return {'FINISHED'} + +class GPTB_OT_sync_visibible_to_render(bpy.types.Operator): + bl_idname = "gp.sync_visibible_to_render" + bl_label = "Sync Overall Viewport Visibility To Render" + bl_description = "Set render visibility from" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + for obj in context.scene.objects: + ## visible_get is the current visibility status combination of hide_viewport and viewlayer hide (eye) + obj.hide_render = not obj.visible_get() + return {'FINISHED'} + +class GPTB_PG_object_visibility(bpy.types.PropertyGroup): + """Property group to handle object visibility""" + is_hidden: BoolProperty( + name="Hide in Viewport", + description="Toggle object visibility in viewport", + get=lambda self: self.get("is_hidden", False), + set=lambda self, value: self.set_visibility(value) + ) + + object_name: StringProperty(name="Object Name") + + def set_visibility(self, value): + """Set the visibility using hide_set()""" + obj = bpy.context.view_layer.objects.get(self.object_name) + if obj: + obj.hide_set(value) + self["is_hidden"] = value + +class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator): + bl_idname = "gp.list_object_visibility_conflicts" + bl_label = "List Object Visibility Conflicts" + bl_description = "List objects visibility conflicts, when viewport and render have different values" + bl_options = {"REGISTER"} + + visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type] + + ## options: + # check_viewlayer : BoolProperty(name="Check Viewlayer", default=False, description="Compare viewlayer (eye) visibility") + # check_viewport : BoolProperty(name="Check Viewport", default=False, description="Compare Viewport (screen icon) visibility") + # check_render : BoolProperty(name="Check Viewport", default=False, description="Compare Render visibility") + + def invoke(self, context, event): + # Clear and rebuild both collections + self.visibility_items.clear() + + # Store objects with conflicts + # objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)] + + + # Create visibility items in same order + for obj in context.scene.objects: # objects_with_conflicts + item = self.visibility_items.add() + item.object_name = obj.name + item["is_hidden"] = obj.hide_get() + + return context.window_manager.invoke_props_dialog(self, width=250) + + def draw(self, context): + layout = self.layout + # row.prop(self, "check_viewlayer") + # row.prop(self, "check_viewport") + # row.prop(self, "check_render") + ## If filtered by prop, displayed list will resize while applying changes ! (not good) + + # Add sync buttons at the top + row = layout.row(align=False) + row.label(text="Sync All Visibility From:") + row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF') + row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF') + row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF') + layout.separator() + + col = layout.column() + # We can safely iterate over visibility_items since objects are stored in same order + for vis_item in self.visibility_items: + obj = context.view_layer.objects.get(vis_item.object_name) + if not obj: + continue + + row = col.row(align=False) + row.label(text=obj.name) + + ## Viewlayer visibility "as prop" to allow slide toggle + # hide_icon='HIDE_ON' if vis_item.is_hidden else 'HIDE_OFF' + hide_icon='HIDE_ON' if obj.hide_get() else 'HIDE_OFF' + row.prop(vis_item, "is_hidden", text="", icon=hide_icon, emboss=False) + + # Direct object properties + row.prop(obj, 'hide_viewport', text='', emboss=False) + row.prop(obj, 'hide_render', text='', emboss=False) + + def execute(self, context): + return {'FINISHED'} ## not exposed in UI, Check is performed in Check file (can be called in popped menu) class GPTB_OT_list_modifier_visibility(bpy.types.Operator): @@ -594,7 +699,13 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator): return {'FINISHED'} classes = ( -GPTB_OT_list_object_visibility, +GPTB_OT_list_viewport_render_visibility, # Only viewport and render +GPTB_OT_sync_visibility_from_viewlayer, +GPTB_OT_sync_visibility_from_viewport, +GPTB_OT_sync_visibility_from_render, +GPTB_OT_sync_visibible_to_render, +GPTB_PG_object_visibility, +GPTB_OT_list_object_visibility_conflicts, GPTB_OT_list_modifier_visibility, GPTB_OT_copy_string_to_clipboard, GPTB_OT_copy_multipath_clipboard, diff --git a/__init__.py b/__init__.py index 3954083..55d86fe 100755 --- a/__init__.py +++ b/__init__.py @@ -625,9 +625,9 @@ class GPTB_prefs(bpy.types.AddonPreferences): layout.label(text='Following checks will be made when clicking "Check File" button:') col = layout.column() col.use_property_split = True - col.prop(self.fixprops, 'check_only') - col.label(text='If dry run is checked, no modification is done', icon='INFO') - col.label(text='Use Ctrl + Click on "Check File" button to invert the behavior', icon='BLANK1') + # col.prop(self.fixprops, 'check_only') + col.label(text='The Popup list possible fixes, you can then use the "Apply Fixes"', icon='INFO') + # col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1') col.separator() col.prop(self.fixprops, 'lock_main_cam') col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})') diff --git a/properties.py b/properties.py index c3ca3d2..38da8dd 100755 --- a/properties.py +++ b/properties.py @@ -31,10 +31,6 @@ def update_layer_name(self, context): class GP_PG_FixSettings(PropertyGroup): - check_only : BoolProperty( - name="Dry run mode (Check only)", - description="Do not change anything, just print the messages", - default=False, options={'HIDDEN'}) lock_main_cam : BoolProperty( name="Lock Main Cam", diff --git a/utils.py b/utils.py index cdab279..070a611 100644 --- a/utils.py +++ b/utils.py @@ -1011,27 +1011,37 @@ def convert_attr(Attr): def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): '''Show message box with element passed as string or list if _message if a list of lists: + if first element is "OPERATOR": + List format: ["OPERATOR", operator_id, text, icon, {prop_name: value, ...}] if sublist have 2 element: - considered a label [text,icon] + considered a label [text, icon] if sublist have 3 element: considered as an operator [ops_id_name, text, icon] + if sublist have 4 element: + considered as a property [object, propname, text, icon] ''' def draw(self, context): + layout = self.layout for l in _message: if isinstance(l, str): - self.layout.label(text=l) - else: - if len(l) == 2: # label with icon - self.layout.label(text=l[0], icon=l[1]) - elif len(l) == 3: # ops - self.layout.operator_context = "INVOKE_DEFAULT" - self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry - - ## offset pnale when using row... - # row = self.layout.row() - # row.label(text=l[1]) - # row.operator(l[0], icon=l[2]) + layout.label(text=l) + elif l[0] == "OPERATOR": # Special operator case with properties + layout.operator_context = "INVOKE_DEFAULT" + op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False) + if len(l) > 4 and isinstance(l[4], dict): + for prop_name, value in l[4].items(): + setattr(op, prop_name, value) + + elif len(l) == 2: # label with icon + layout.label(text=l[0], icon=l[1]) + elif len(l) == 3: # ops + layout.operator_context = "INVOKE_DEFAULT" + layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry + elif len(l) == 4: # prop + row = layout.row(align=True) + row.label(text=l[2], icon=l[3]) + row.prop(l[0], l[1], text='') if isinstance(_message, str): _message = [_message]