6 Commits
Author SHA1 Message Date
pullusb 7665dd4f4f Add optional object target for constant cursor follow 2024-12-03 16:37:42 +01:00
pullusb 2013c55ba8 Fix broken cursor_follow feature 2024-12-03 16:10:30 +01:00
pullusb 3695470354 Fix problem in viz conflict check 2024-12-03 15:36:55 +01:00
pullusb 186229cdba typo 2024-12-03 14:36:48 +01:00
pullusb e441f485a6 version bump 2024-12-03 14:27:35 +01:00
pullusb 0cee6163aa 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
2024-12-03 14:26:43 +01:00
7 changed files with 226 additions and 97 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog # Changelog
4.0.3
changed: File checker doest not fix directly when clicked (also removed choice in preference):
- 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 4.0.1
- fixed: layer nav operator on page up/down - fixed: layer nav operator on page up/down
+19 -13
View File
@@ -114,13 +114,17 @@ prev_matrix = None
## used in properties file to register in boolprop update ## used in properties file to register in boolprop update
def cursor_follow_update(self, context): def cursor_follow_update(self, context):
'''append or remove cursor_follow handler according a boolean''' '''append or remove cursor_follow handler according a boolean'''
ob = bpy.context.object
if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
global prev_matrix global prev_matrix
# imported in properties to register in boolprop update # imported in properties to register in boolprop update
if self.cursor_follow:#True if self.cursor_follow:#True
if ob:
# out of below condition to be called when setting target as well
prev_matrix = ob.matrix_world.copy()
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]: if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
if context.object:
prev_matrix = context.object.matrix_world
bpy.app.handlers.frame_change_post.append(cursor_follow) bpy.app.handlers.frame_change_post.append(cursor_follow)
else:#False else:#False
@@ -131,11 +135,13 @@ def cursor_follow_update(self, context):
def cursor_follow(scene): def cursor_follow(scene):
'''Handler to make the cursor follow active object matrix changes on frame change''' '''Handler to make the cursor follow active object matrix changes on frame change'''
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)... ob = bpy.context.object
if not bpy.context.object: if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
if not ob:
return return
global prev_matrix global prev_matrix
ob = bpy.context.object
current_matrix = ob.matrix_world current_matrix = ob.matrix_world
if not prev_matrix: if not prev_matrix:
prev_matrix = current_matrix.copy() prev_matrix = current_matrix.copy()
@@ -149,8 +155,6 @@ def cursor_follow(scene):
## translation only ## translation only
# scene.cursor.location += (current_matrix - prev_matrix).to_translation() # scene.cursor.location += (current_matrix - prev_matrix).to_translation()
# print('offset:', (current_matrix - prev_matrix).to_translation())
## full ## full
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location) scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
@@ -164,11 +168,13 @@ def selection_changed():
"""Callback function for selection changes""" """Callback function for selection changes"""
if not bpy.context.scene.gptoolprops.cursor_follow: if not bpy.context.scene.gptoolprops.cursor_follow:
return return
print('select_changed') if bpy.context.scene.gptoolprops.cursor_follow_target:
# we are following a target, nothing to update on selection change
return
global prev_matrix, prev_active_obj global prev_matrix, prev_active_obj
if prev_active_obj != bpy.context.object: if prev_active_obj != bpy.context.object:
## Set stored matrix to active object ## Set stored matrix to active object
prev_matrix = bpy.context.object.matrix_world prev_matrix = bpy.context.object.matrix_world.copy()
prev_active_obj = bpy.context.object prev_active_obj = bpy.context.object
## Note: Same owner as layer manager (will be removed as well) ## Note: Same owner as layer manager (will be removed as well)
@@ -197,15 +203,15 @@ def register():
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap # swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
## Follow cursor matrix update on object change ## Follow cursor matrix update on object change
# bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
# ## Directly set msgbus to work at first addon activation # select_change # ## Directly set msgbus to work at first addon activation # select_change
# bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow' ## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
def unregister(): def unregister():
# bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap # swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
+161 -60
View File
@@ -4,6 +4,11 @@ from pathlib import Path
import numpy as np import numpy as np
from . import utils from . import utils
from bpy.props import (BoolProperty,
PointerProperty,
CollectionProperty,
StringProperty)
def remove_stroke_exact_duplications(apply=True): def remove_stroke_exact_duplications(apply=True):
'''Remove accidental stroke duplication (points exactly in the same place) '''Remove accidental stroke duplication (points exactly in the same place)
:apply: Remove the duplication instead of just listing dupes :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 # Disable use light on all object
# Remove redundant strokes in frames # 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): def invoke(self, context, event):
# need some self-control (I had to...) # need some self-control (I had to...)
self.ctrl = event.ctrl self.ctrl = event.ctrl
@@ -63,10 +72,13 @@ class GPTB_OT_file_checker(bpy.types.Operator):
fix = prefs.fixprops fix = prefs.fixprops
problems = [] problems = []
apply = not fix.check_only ## Old method : Apply fixes based on pref (inverted by ctrl key)
# # If Ctrl is pressed, invert behavior (invert boolean)
# apply ^= self.ctrl
# If Ctrl is pressed, invert behavior (invert boolean) apply = self.apply_fixes
apply ^= self.ctrl if self.ctrl:
apply = True
## Lock main cam: ## Lock main cam:
if fix.lock_main_cam: if fix.lock_main_cam:
@@ -169,13 +181,14 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if fix.list_obj_vis_conflict: if fix.list_obj_vis_conflict:
viz_ct = 0 viz_ct = 0
for o in context.scene.objects: 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' vp = 'No' if o.hide_viewport else 'Yes'
rd = 'No' if o.hide_render else 'Yes' rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1 viz_ct += 1
print(f'{o.name} : viewport {vp} != render {rd}') print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
if viz_ct: 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 ## GP modifiers visibility conflict
if fix.list_gp_mod_vis_conflict: if fix.list_gp_mod_vis_conflict:
@@ -281,8 +294,12 @@ class GPTB_OT_file_checker(bpy.types.Operator):
else: else:
print(p[0]) 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 # 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') utils.show_message_box(problems, _title = title, _icon = 'INFO')
else: else:
self.report({'INFO'}, 'All good') self.report({'INFO'}, 'All good')
@@ -482,45 +499,10 @@ class GPTB_OT_links_checker(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self, width=popup_width) 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): class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
all_lnks = [] bl_idname = "gp.list_viewport_render_visibility"
has_broken_link = False bl_label = "List Viewport And Render Visibility Conflicts"
## 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"
bl_description = "List objects visibility conflicts, when viewport and render have different values" bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@@ -539,20 +521,133 @@ class GPTB_OT_list_object_visibility(bpy.types.Operator):
def execute(self, context): def execute(self, context):
return {'FINISHED'} return {'FINISHED'}
## basic listing as message box # all in invoke now
# li = [] ### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
# viz_ct = 0 class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
# for o in context.scene.objects: bl_idname = "gp.sync_visibility_from_viewlayer"
# if o.hide_viewport != o.hide_render: bl_label = "Sync Visibility From Viewlayer"
# vp = 'No' if o.hide_viewport else 'Yes' bl_description = "Set viewport and render visibility to match viewlayer visibility"
# rd = 'No' if o.hide_render else 'Yes' bl_options = {"REGISTER", "UNDO"}
# viz_ct += 1
# li.append(f'{o.name} : viewport {vp} != render {rd}') def execute(self, context):
# if li: for obj in context.scene.objects:
# utils.show_message_box(_message=li, _title=f'{viz_ct} visibility conflicts found') is_hidden = obj.hide_get() # Get viewlayer visibility
# else: obj.hide_viewport = is_hidden
# self.report({'INFO'}, f"No Object visibility conflict on current scene") obj.hide_render = is_hidden
# return {'FINISHED'} 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]
def invoke(self, context, event):
# Clear and rebuild both collections
self.visibility_items.clear()
# Store objects with conflicts
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ?
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 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
# 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' # based on object state
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) ## 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): class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
@@ -594,7 +689,13 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
classes = ( 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_list_modifier_visibility,
GPTB_OT_copy_string_to_clipboard, GPTB_OT_copy_string_to_clipboard,
GPTB_OT_copy_multipath_clipboard, GPTB_OT_copy_multipath_clipboard,
+2
View File
@@ -281,6 +281,8 @@ class GPTB_PT_anim_manager(Panel):
col.use_property_split = False col.use_property_split = False
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR') text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon) col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
if context.scene.gptoolprops.cursor_follow:
col.prop(context.scene.gptoolprops, 'cursor_follow_target', text='Target', icon='OBJECT_DATA')
class GPTB_PT_toolbox_playblast(Panel): class GPTB_PT_toolbox_playblast(Panel):
+4 -4
View File
@@ -4,7 +4,7 @@ bl_info = {
"name": "GP toolbox", "name": "GP toolbox",
"description": "Tool set for Grease Pencil in animation production", "description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux", "author": "Samuel Bernou, Christophe Seux",
"version": (4, 0, 2), "version": (4, 0, 3),
"blender": (4, 3, 0), "blender": (4, 3, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "", "warning": "",
@@ -625,9 +625,9 @@ class GPTB_prefs(bpy.types.AddonPreferences):
layout.label(text='Following checks will be made when clicking "Check File" button:') layout.label(text='Following checks will be made when clicking "Check File" button:')
col = layout.column() col = layout.column()
col.use_property_split = True col.use_property_split = True
col.prop(self.fixprops, 'check_only') # col.prop(self.fixprops, 'check_only')
col.label(text='If dry run is checked, no modification is done', icon='INFO') 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" button to invert the behavior', icon='BLANK1') # col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1')
col.separator() col.separator()
col.prop(self.fixprops, 'lock_main_cam') 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})') col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})')
+5 -4
View File
@@ -31,10 +31,6 @@ def update_layer_name(self, context):
class GP_PG_FixSettings(PropertyGroup): 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( lock_main_cam : BoolProperty(
name="Lock Main Cam", name="Lock Main Cam",
@@ -182,6 +178,11 @@ class GP_PG_ToolsSettings(PropertyGroup):
name='Cursor Follow', description="3D cursor follow active object animation when activated", name='Cursor Follow', description="3D cursor follow active object animation when activated",
default=False, update=cursor_follow_update) default=False, update=cursor_follow_update)
cursor_follow_target : bpy.props.PointerProperty(
name='Cursor Follow Target',
description="Optional target object to follow for cursor instead of active object",
type=bpy.types.Object, update=cursor_follow_update)
## gpv3 : no edit line color anymore ## gpv3 : no edit line color anymore
# edit_lines_opacity : FloatProperty( # edit_lines_opacity : FloatProperty(
# name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils", # name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils",
+22 -12
View File
@@ -1011,27 +1011,37 @@ def convert_attr(Attr):
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
'''Show message box with element passed as string or list '''Show message box with element passed as string or list
if _message if a list of lists: 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: if sublist have 2 element:
considered a label [text,icon] considered a label [text, icon]
if sublist have 3 element: if sublist have 3 element:
considered as an operator [ops_id_name, text, icon] 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): def draw(self, context):
layout = self.layout
for l in _message: for l in _message:
if isinstance(l, str): if isinstance(l, str):
self.layout.label(text=l) layout.label(text=l)
else: elif l[0] == "OPERATOR": # Special operator case with properties
if len(l) == 2: # label with icon layout.operator_context = "INVOKE_DEFAULT"
self.layout.label(text=l[0], icon=l[1]) op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False)
elif len(l) == 3: # ops if len(l) > 4 and isinstance(l[4], dict):
self.layout.operator_context = "INVOKE_DEFAULT" for prop_name, value in l[4].items():
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry setattr(op, prop_name, value)
## offset pnale when using row... elif len(l) == 2: # label with icon
# row = self.layout.row() layout.label(text=l[0], icon=l[1])
# row.label(text=l[1]) elif len(l) == 3: # ops
# row.operator(l[0], icon=l[2]) 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): if isinstance(_message, str):
_message = [_message] _message = [_message]