# SPDX-License-Identifier: GPL-2.0-or-later bl_info = { "name": "Property Chart", "author": "Campbell Barton (ideasman42)", "version": (0, 1, 1), "blender": (2, 80, 0), "location": "View3D > Sidebar > Item Tab", "description": ("Edit arbitrary selected properties for " "objects/sequence strips of the same type"), "warning": "", "doc_url": "{BLENDER_MANUAL_URL}/addons/system/property_chart.html", "category": "System", } """List properties of selected objects""" import bpy from bl_operators.presets import AddPresetBase from bpy.types import ( Menu, Operator, Panel, ) class AddPresetProperties(AddPresetBase, Operator): """Add an properties preset""" bl_idname = "scene.properties_preset_add" bl_label = "Add Properties Preset" preset_menu = "SCENE_MT_properties_presets" preset_defines = [ "scene = bpy.context.scene", ] def pre_cb(self, context): space_type = context.space_data.type if space_type == 'VIEW_3D': self.preset_subdir = "system_property_chart_view3d" self.preset_values = ["scene.view3d_edit_props"] else: self.preset_subdir = "system_property_chart_sequencer" self.preset_values = ["scene.sequencer_edit_props"] class SCENE_MT_properties_presets(Menu): bl_label = "Properties Presets" preset_operator = "script.execute_preset" def draw(self, context): space_type = context.space_data.type if space_type == 'VIEW_3D': self.preset_subdir = "system_property_chart_view3d" else: self.preset_subdir = "system_property_chart_sequencer" Menu.draw_preset(self, context) def _property_chart_data_get(self, context): # eg. context.active_object obj = eval("context.%s" % self.context_data_path_active) if obj is None: return None, None # eg. context.selected_objects[:] selected_objects = eval("context.%s" % self.context_data_path_selected)[:] if not selected_objects: return None, None return obj, selected_objects def _property_chart_draw(self, context): """ This function can run for different types. """ obj, selected_objects = _property_chart_data_get(self, context) if not obj: return # active first try: active_index = selected_objects.index(obj) except ValueError: active_index = -1 if active_index > 0: # not the first already selected_objects[0], selected_objects[active_index] = selected_objects[active_index], selected_objects[0] id_storage = context.scene strings = getattr(id_storage, self._PROP_STORAGE_ID) # Collected all props, now display them all layout = self.layout if strings: def obj_prop_get(obj, attr_string): """return a pair (rna_base, "rna_property") to give to the rna UI property function""" attrs = attr_string.split(".") val_new = obj for i, attr in enumerate(attrs): val_old = val_new val_new = getattr(val_old, attr, Ellipsis) if val_new == Ellipsis: return None, None return val_old, attrs[-1] strings = strings.split() prop_all = [] for obj in selected_objects: prop_pairs = [] prop_found = False for attr_string in strings: prop_pairs.append(obj_prop_get(obj, attr_string)) if prop_found is False and prop_pairs[-1] != (None, None): prop_found = True if prop_found: prop_all.append((obj, prop_pairs)) row = layout.row(align=True) col = row.column(align=True) col.label(text="name") for obj, prop_pairs in prop_all: col.prop(obj, "name", text="") for i in range(len(strings)): col = row.column(align=True) # name and copy button rowsub = col.row(align=False) rowsub.label(text=strings[i].rsplit(".", 1)[-1]) props = rowsub.operator("wm.chart_copy", text="", icon='PASTEDOWN', emboss=False) props.data_path_active = self.context_data_path_active props.data_path_selected = self.context_data_path_selected props.data_path = strings[i] for obj, prop_pairs in prop_all: data, attr = prop_pairs[i] # row is needed for vector buttons if data: col.row().prop(data, attr, text="") # , emboss=obj==active_object else: col.label(text="") # Presets for properties col = layout.column() col.label(text="Properties") row = col.row(align=True) row.menu("SCENE_MT_properties_presets", text=bpy.types.SCENE_MT_properties_presets.bl_label) row.operator("scene.properties_preset_add", text="", icon='ADD') row.operator("scene.properties_preset_add", text="", icon='REMOVE').remove_active = True # edit the display props col.prop(id_storage, self._PROP_STORAGE_ID, text="") class View3DEditProps(Panel): bl_idname = "SYSPROP_CHART_PT_view3d" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Item" bl_label = "Property Chart" bl_context = "objectmode" bl_options = {'DEFAULT_CLOSED'} _PROP_STORAGE_ID = "view3d_edit_props" _PROP_STORAGE_ID_DESCR = "Properties of objects in the context" _PROP_STORAGE_DEFAULT = "data data.use_auto_smooth" # _property_chart_draw needs these context_data_path_active = "active_object" context_data_path_selected = "selected_objects" draw = _property_chart_draw class SequencerEditProps(Panel): bl_idname = "SYSPROP_CHART_PT_sequencer" bl_space_type = 'SEQUENCE_EDITOR' bl_region_type = 'UI' bl_category = "Item" bl_label = "Property Chart" bl_options = {'DEFAULT_CLOSED'} _PROP_STORAGE_ID = "sequencer_edit_props" _PROP_STORAGE_ID_DESCR = "Properties of sequencer strips in the context" _PROP_STORAGE_DEFAULT = "blend_type blend_alpha" # _property_chart_draw needs these context_data_path_active = "scene.sequence_editor.active_strip" context_data_path_selected = "selected_sequences" draw = _property_chart_draw @classmethod def poll(cls, context): return context.scene.sequence_editor is not None # Operator to copy properties def _property_chart_copy(self, context): obj, selected_objects = _property_chart_data_get(self, context) if not obj: return data_path = self.data_path data_path_with_dot = data_path if not data_path_with_dot.startswith("["): data_path_with_dot = "." + data_path_with_dot code = compile( "obj_iter%s = obj%s" % (data_path_with_dot, data_path_with_dot), "", 'exec', ) for obj_iter in selected_objects: if obj != obj_iter: try: exec(code, {}, {"obj": obj, "obj_iter": obj_iter}) except: # just in case we need to know what went wrong! import traceback traceback.print_exc() from bpy.props import StringProperty class CopyPropertyChart(Operator): "Open a path in a file browser" bl_idname = "wm.chart_copy" bl_label = "Copy properties from active to selected" data_path_active: StringProperty() data_path_selected: StringProperty() data_path: StringProperty() def execute(self, context): # so attributes are found for '_property_chart_data_get()' self.context_data_path_active = self.data_path_active self.context_data_path_selected = self.data_path_selected _property_chart_copy(self, context) return {'FINISHED'} # List The Classes # classes = ( AddPresetProperties, SCENE_MT_properties_presets, View3DEditProps, SequencerEditProps, CopyPropertyChart ) def register(): for cls in classes: bpy.utils.register_class(cls) Scene = bpy.types.Scene for cls in View3DEditProps, SequencerEditProps: setattr( Scene, cls._PROP_STORAGE_ID, StringProperty( name="Properties", description=cls._PROP_STORAGE_ID_DESCR + " (separated by spaces)", default=cls._PROP_STORAGE_DEFAULT, maxlen=1024, ), ) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) Scene = bpy.types.Scene for cls in View3DEditProps, SequencerEditProps: delattr(Scene, cls._PROP_STORAGE_ID, ) if __name__ == "__main__": register()