├── LICENSE ├── README.md ├── __init__.py ├── mccolutils.py ├── mcdata.py ├── mcitems.py ├── mcmenu.py ├── mcops.py ├── mcpanels.py ├── mcsettings.py └── mcutils.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mustard2 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Menu Creator 2 | A Blender addon to quickly create custom menus for any Object. 3 | 4 | This addon can be useful both for Blender users, to expose the properties in a handy and clean Menu, and for content creators who want to include a Menu to their models. Moreover, in the latter case, since the Menu is fully customizable, users of these models will also be able to furtherly customize the Menu without knowing anything about Python! 5 | 6 | ## Features 7 | - create a menu for any Blender object (one for each Object you want) 8 | 9 | ![Prev2](https://i.ibb.co/Dw3sDLH/Eqh-COxv-Xc-AAqd1-H.jpg) 10 | 11 | - plenty customization options (more to come) 12 | 13 | ![Prev1](https://i.ibb.co/mXdrYWg/Eqh-CLG3-Xc-AUYe3v.jpg) 14 | 15 | - add all the properties you want to the menu simply right clicking on any property 16 | 17 | - link properties so they always have the same value 18 | 19 | - collection functionality: you can create a list of collections from which the menu will list the contained objects 20 | 21 | - outfit functionality: as an extension of the collection functionality, it is possible to enable auto-masking of the objects listed in the menu. This means that you can define a Body object, and its mask modifiers will be activated when you show an Object in the collection list 22 | 23 | ## Installation 24 | You can find the latest version in the [Releases page of this repository](https://github.com/Mustard2/MenuCreator/releases). 25 | 26 | To install the add-on, download the source code as a zip file and install it through Blender's preferences. 27 | 28 | ## Instructions 29 | - create a Menu clicking on an Object and initializing the Menu 30 | - add properties simply right clicking on any Blender property, and clicking Add to the Menu (note that operators are not supported at the moment, and can not be added to the Menu) 31 | 32 | ![Right-click](https://i.ibb.co/V3F2x9w/Eqh-CQo-GXUAE9pad.jpg) 33 | 34 | - to start editing the Menu, be sure to be in Edit Mode (check the Menu Edit Mode Enable in the Menu Creator settings) 35 | 36 | - you can change the name and the icon of the properties in Edit Mode, clicking on the cogwheel 37 | 38 | - you can link properties to the ones you already added. Just click on a property and use Link to Property 39 | 40 | ![Linked props](https://i.ibb.co/wN929LX/Cattura2.png) 41 | ![Linked props2](https://i.ibb.co/HNFVf15/Cattura3.png) 42 | 43 | - you can create new sections in the Menu to get a cleaner list of the properties, clicking on the + button. You can choose the order of both sections and properties within the section 44 | 45 | - to use the collection functionality, create a section and choose Collection Section (note that the type of the section can not be changed after the section has been created) 46 | 47 | - to enable the global collection options and the Outfit functionalities, check the Section properties (the cogwheel near the Section name in Edit Mode) 48 | 49 | ![Right-click Collection](https://i.ibb.co/7CT96KK/Eqh-CUMKXAAEB36u.jpg) 50 | 51 | - [check the video for the complete instructions](https://gofile.io/d/NPrmDS) (updated to version 0.0.1) 52 | 53 | ## FAQ 54 | 55 | - *I shared a blend file to another person and he/she can't see the Menu* 56 | 57 | All the users of these Menu should install this addon! 58 | 59 | - *How can I add a property of an Object to the menu of another Object?* 60 | 61 | By default, when you click on an Object, the addon will show the menu of the selected item. To pin the current Object Menu click on the Pin button on the first line (on the far left), and this will prevent the Menu to be changed when you select other Objects until you un-pin the Menu. 62 | 63 | - *How can I show only one Object Menu, i.e. why does the Menu changes when I click on other Objects?* 64 | 65 | This is the intended behaviour. To pin the current Object Menu click on the Pin button on the first line (on the far left), and this will prevent the Menu to be changed when you select other Objects until you un-pin the Menu. 66 | 67 | - *I found an issue or I would like to suggest new functionalities* 68 | 69 | You can open an Issue on GitHub (click on the Issue tab above). Please, try to be specific and include screenshots if possible. 70 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Mustard Menu Creator addon 2 | # https://github.com/Mustard2/MenuCreator 3 | 4 | bl_info = { 5 | "name": "Menu Creator", 6 | "description": "Create a custom menu for each Object. To add properties or collections, just right click on the properties and hit Add property to the Menu", 7 | "author": "Mustard", 8 | "version": (0, 0, 3), 9 | "blender": (2, 91, 0), 10 | "warning": "", 11 | "wiki_url": "https://github.com/Mustard2/MenuCreator", 12 | "category": "User Interface", 13 | } 14 | 15 | import bpy 16 | from bpy.app.handlers import persistent 17 | from .mcdata import * 18 | from . import mcitems 19 | from . import mcops 20 | from . import mcpanels 21 | from . import mcsettings 22 | from . import mcmenu 23 | 24 | # USER INTERFACE 25 | 26 | # Handlers 27 | 28 | @persistent 29 | def mc_scene_modification_handler(scene): 30 | """Called at every modification done to the scene.""" 31 | 32 | for obj in bpy.data.objects: 33 | 34 | # Handler for linked custom properties 35 | for prop in obj.mc_properties: 36 | for link_prop in prop.linked_props: 37 | if '].[' in link_prop.path + '.' + link_prop.id: 38 | exec(link_prop.path + link_prop.id + '=' + prop.path + '.' + prop.id) 39 | else: 40 | exec(link_prop.path + '.' + link_prop.id + '=' + prop.path + '.' + prop.id) 41 | 42 | # Part checking for changes in the list collection 43 | # This is needed to ensure a clean list against deletion of collections from the outliner 44 | for sec in obj.mc_sections: 45 | i = 0 46 | for el in sec.collections: 47 | if not hasattr(el.collection, 'name'): 48 | sec.collections.remove(i) 49 | i = i + 1 50 | 51 | modules = ( 52 | mcsettings, 53 | mcitems, 54 | mcops, 55 | mcpanels, 56 | mcmenu 57 | ) 58 | 59 | def register(): 60 | 61 | for mod in modules: 62 | mod.register() 63 | 64 | # Handlers 65 | bpy.app.handlers.depsgraph_update_post.append(mc_scene_modification_handler) 66 | bpy.app.handlers.redo_post.append(mc_scene_modification_handler) 67 | bpy.app.handlers.undo_post.append(mc_scene_modification_handler) 68 | 69 | def unregister(): 70 | # Handlers 71 | bpy.app.handlers.undo_post.remove(mc_scene_modification_handler) 72 | bpy.app.handlers.redo_post.remove(mc_scene_modification_handler) 73 | bpy.app.handlers.depsgraph_update_post.remove(mc_scene_modification_handler) 74 | 75 | for mod in reversed(modules): 76 | mod.unregister() 77 | 78 | if __name__ == "__main__": 79 | register() 80 | -------------------------------------------------------------------------------- /mccolutils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | # ---- Properties only functions 4 | 5 | # Function to remove a specific property from the collection 6 | # Return 1 if the property was found and deleted 7 | def mc_remove_property_item(collection, item): 8 | i=-1 9 | for el in collection: 10 | i=i+1 11 | if el.path == item[1] and el.id == item[2]: 12 | break 13 | if i>=0: 14 | collection.remove(i) 15 | 16 | return i>=0 17 | 18 | # Function to add a specific property to the collection, if not already there 19 | # Return 0 if the property has not been added because already in the properties list 20 | def mc_add_property_item(collection, item): 21 | i=True 22 | for el in collection: 23 | if el.path == item[1] and el.id == item[2]: 24 | i=False 25 | break 26 | if i: 27 | add_item = collection.add() 28 | add_item.name = item[0] 29 | add_item.path = item[1] 30 | add_item.id = item[2] 31 | add_item.mc_id = mc_len_collection(collection) 32 | 33 | return i 34 | 35 | # Function to find the index of a property 36 | def mc_find_index(collection, item): 37 | i=-1 38 | for el in collection: 39 | i=i+1 40 | if el.path == item[1] and el.id == item[2]: 41 | break 42 | return i 43 | 44 | # Function to clean properties of a single object 45 | def mc_clean_single_properties(obj): 46 | obj.mc_properties.clear() 47 | 48 | # Function to clean all the properties of every object 49 | def mc_clean_properties(): 50 | for obj in bpy.data.objects: 51 | obj.mc_properties.clear() 52 | 53 | # Function to print the properties 54 | def mc_print_properties(): 55 | for obj in bpy.data.objects: 56 | for el in obj.mc_properties: 57 | print(el.id + " : property" + el.name + " with path "+el.path) 58 | 59 | # Function to iutput the ID of the element 60 | def mc_prop_ID(elem): 61 | return elem.mc_id 62 | 63 | # ---- Sections only functions 64 | 65 | # Function to create an array of tuples for enum properties 66 | def mc_section_list(scene, context): 67 | 68 | settings = bpy.context.scene.mc_settings 69 | if settings.em_fixobj: 70 | obj = settings.em_fixobj_pointer 71 | else: 72 | obj = context.active_object 73 | 74 | items = [] 75 | 76 | i = 0 77 | for el in obj.mc_sections: 78 | if el.type == "DEFAULT": 79 | items.append( (el.name,el.name,el.name,el.icon,i) ) 80 | i = i + 1 81 | 82 | return items 83 | 84 | # Function to clean sections of a single object 85 | def mc_clean_single_sections(obj): 86 | obj.mc_sections.clear() 87 | 88 | # Function to clean the sections of every object 89 | def mc_clean_sections(): 90 | for obj in bpy.data.objects: 91 | obj.mc_sections.clear() 92 | 93 | # Function to find the index of a section from the name 94 | def mc_find_index_section(collection, item): 95 | i=-1 96 | for el in collection: 97 | i=i+1 98 | if el.name == item: 99 | break 100 | return i 101 | 102 | # Function to find the index of a section from the ID 103 | def mc_find_index_section_fromID(collection, item): 104 | i=-1 105 | for el in collection: 106 | i=i+1 107 | if el.id == item: 108 | break 109 | return i 110 | 111 | # Function to iutput the ID of the element 112 | def mc_sec_ID(elem): 113 | return elem.id 114 | 115 | # ---- Sections and properties functions 116 | 117 | # Function to find the length of a collection 118 | def mc_len_collection(collection): 119 | i=0 120 | for el in collection: 121 | i=i+1 122 | return i 123 | -------------------------------------------------------------------------------- /mcdata.py: -------------------------------------------------------------------------------- 1 | 2 | # Arrays for ENUM properties 3 | # Array to store different section type 4 | mc_section_type_list = [ 5 | ("DEFAULT","Standard","A simple collection of properties that can be added right clicking on fields -> Add Property to the Menu"), 6 | ("COLLECTION","Collection List","Right clicking on them in the Outliner, you can add collections whose elements can be shown/hidden in the Menu. Only one collection will be shown at the same time.\nIdeal for: Outfit lists","OUTLINER_COLLECTION",1) 7 | ] 8 | # Array to store possible icons to be used by properties and sections 9 | mc_icon_list = [ 10 | ("NONE","No Icon","No Icon"), 11 | ("USER", "Face", "Face","USER",1), 12 | ("HAIR", "Hair", "Hair","HAIR",2), 13 | ("MOD_CLOTH", "Cloth", "Cloth","MOD_CLOTH",3), 14 | ("MATERIAL", "Material", "Material","MATERIAL",4), 15 | ("ARMATURE_DATA", "Armature", "Armature","ARMATURE_DATA",5), 16 | ("MOD_ARMATURE", "Armature", "Armature","MOD_ARMATURE",6), 17 | ("EXPERIMENTAL", "Experimental", "Experimental","EXPERIMENTAL",7), 18 | ("WORLD", "World", "World","WORLD",8), 19 | ("PARTICLEMODE", "Comb", "Comb","PARTICLEMODE",9) 20 | ] -------------------------------------------------------------------------------- /mcitems.py: -------------------------------------------------------------------------------- 1 | 2 | import bpy 3 | from . import mcdata 4 | 5 | # Class to store collections for section informations 6 | class MCCollectionItem(bpy.types.PropertyGroup): 7 | collection : bpy.props.PointerProperty(name="Collection",type=bpy.types.Collection) 8 | 9 | 10 | # Class to store section informations 11 | class MCSectionItem(bpy.types.PropertyGroup): 12 | 13 | # Properties and update functions 14 | # Function to update the collapsed status if the collapsed section property is changed 15 | def mc_sections_collapsed_update(self, context): 16 | 17 | if not self.collapsable: 18 | self.collapsed = False 19 | 20 | return 21 | 22 | # Function to create an array of tuples for enum collections 23 | def mc_collections_list(self, context): 24 | 25 | items = [] 26 | 27 | for el in self.collections: 28 | if hasattr(el.collection, 'name'): 29 | items.append( (el.collection.name,el.collection.name,el.collection.name) ) 30 | 31 | return sorted(items) 32 | 33 | # Function to update global collection properties 34 | def mc_collections_list_update(self, context): 35 | 36 | for collection in self.collections: 37 | if collection.collection.name == self.collections_list: 38 | collection.collection.hide_viewport = False 39 | collection.collection.hide_render = False 40 | else: 41 | collection.collection.hide_viewport = True 42 | collection.collection.hide_render = True 43 | 44 | def mc_collections_global_options_update(self, context): 45 | 46 | items = [] 47 | 48 | i = 0 49 | for el in self.collections: 50 | for obj in el.collection.objects: 51 | 52 | if obj.type == "MESH": 53 | obj.data.use_auto_smooth = self.collections_global_normalautosmooth 54 | 55 | for modifier in obj.modifiers: 56 | if modifier.type == "CORRECTIVE_SMOOTH": 57 | modifier.show_viewport = self.collections_global_smoothcorrection 58 | modifier.show_render = self.collections_global_smoothcorrection 59 | elif modifier.type == "MASK": 60 | modifier.show_viewport = self.collections_global_mask 61 | modifier.show_render = self.collections_global_mask 62 | elif modifier.type == "SHRINKWRAP": 63 | modifier.show_viewport = self.collections_global_shrinkwrap 64 | modifier.show_render = self.collections_global_shrinkwrap 65 | 66 | if self.outfit_enable: 67 | for modifier in self.outfit_body.modifiers: 68 | if modifier.type == "MASK": 69 | if not self.collections_global_mask: 70 | modifier.show_viewport = False 71 | modifier.show_render = False 72 | else: 73 | for el in self.collections: 74 | for obj in el.collection.objects: 75 | if obj.name in modifier.name and not obj.hide_viewport: 76 | modifier.show_viewport = True 77 | modifier.show_render = True 78 | 79 | return 80 | 81 | # Poll function for the selection of mesh only in pointer properties 82 | def mc_poll_mesh(self, object): 83 | return object.type == 'MESH' 84 | 85 | 86 | # Global section options 87 | id : bpy.props.IntProperty(name="Section ID") 88 | name : bpy.props.StringProperty(name="Section Name") 89 | icon : bpy.props.StringProperty(name="Section Icon", default="") 90 | type : bpy.props.StringProperty(name="Section Type", default="DEFAULT") 91 | collapsable : bpy.props.BoolProperty(name="Section Collapsable", default=False, update=mc_sections_collapsed_update) 92 | 93 | # Global section option enforcer 94 | collapsed : bpy.props.BoolProperty(name="", default = False, description="") 95 | 96 | # COLLECTION type options 97 | collections_enable_global_smoothcorrection: bpy.props.BoolProperty(default=False) 98 | collections_enable_global_shrinkwrap: bpy.props.BoolProperty(default=False) 99 | collections_enable_global_mask: bpy.props.BoolProperty(default=False) 100 | collections_enable_global_normalautosmooth: bpy.props.BoolProperty(default=False) 101 | # COLLECTION type data 102 | collections: bpy.props.CollectionProperty(name="Section Collection List", type=MCCollectionItem) 103 | collections_list: bpy.props.EnumProperty(name="Section Collection List", items = mc_collections_list, update=mc_collections_list_update) 104 | collections_global_smoothcorrection: bpy.props.BoolProperty(name="Smooth Correction", default=True, update=mc_collections_global_options_update) 105 | collections_global_shrinkwrap: bpy.props.BoolProperty(name="Shrinkwrap", default=True, update=mc_collections_global_options_update) 106 | collections_global_mask: bpy.props.BoolProperty(name="Mask", default=True, update=mc_collections_global_options_update) 107 | collections_global_normalautosmooth: bpy.props.BoolProperty(name="Normals Auto Smooth", default=True, update=mc_collections_global_options_update) 108 | # Outfit variant 109 | outfit_enable : bpy.props.BoolProperty(name="Outfit", default=False) 110 | outfit_body : bpy.props.PointerProperty(name="Outfit Body", description = "The masks of this object will be switched on/off depending on which elements of the collections visibility", type=bpy.types.Object, poll=mc_poll_mesh) 111 | 112 | 113 | # Class to store linked properties informations 114 | class MCLinkedPropertyItem(bpy.types.PropertyGroup): 115 | path: bpy.props.StringProperty(name="Property Path") 116 | id : bpy.props.StringProperty(name="Property Identifier") 117 | 118 | # Class to store properties informations 119 | class MCPropertyItem(bpy.types.PropertyGroup): 120 | mc_id : bpy.props.IntProperty(name="Section ID") 121 | name : bpy.props.StringProperty(name="Property Name") 122 | path: bpy.props.StringProperty(name="Property Path") 123 | id : bpy.props.StringProperty(name="Property Identifier") 124 | icon : bpy.props.EnumProperty(name="Property Icon", default="NONE",items=mcdata.mc_icon_list) 125 | section : bpy.props.StringProperty(name="Section", default="Unsorted") 126 | hide : bpy.props.BoolProperty(name="Hide Property", default=False) 127 | linked_props: bpy.props.CollectionProperty(name="Linked properties", type=MCLinkedPropertyItem) 128 | 129 | 130 | def register(): 131 | bpy.utils.register_class(MCCollectionItem) 132 | bpy.utils.register_class(MCSectionItem) 133 | bpy.utils.register_class(MCLinkedPropertyItem) 134 | bpy.utils.register_class(MCPropertyItem) 135 | bpy.types.Object.mc_properties = bpy.props.CollectionProperty(type=MCPropertyItem) 136 | bpy.types.Object.mc_sections = bpy.props.CollectionProperty(type=MCSectionItem) 137 | 138 | def unregister(): 139 | del bpy.types.Object.mc_properties 140 | del bpy.types.Object.mc_sections 141 | bpy.utils.unregister_class(MCSectionItem) 142 | bpy.utils.unregister_class(MCLinkedPropertyItem) 143 | bpy.utils.unregister_class(MCPropertyItem) 144 | bpy.utils.unregister_class(MCCollectionItem) -------------------------------------------------------------------------------- /mcmenu.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import mcops 3 | 4 | class WM_MT_button_context(bpy.types.Menu): 5 | bl_label = "Custom Action" 6 | 7 | def draw(self, context): 8 | pass 9 | 10 | def menu_func(self, context): 11 | 12 | if hasattr(context, 'button_prop'): 13 | layout = self.layout 14 | layout.separator() 15 | layout.operator(mcops.MC_AddProperty.bl_idname) 16 | 17 | def menu_func_link(self, context): 18 | 19 | if hasattr(context, 'button_prop'): 20 | layout = self.layout 21 | #layout.label(text="Try") 22 | self.layout.menu(OUTLINER_MT_link_mcmenu.bl_idname) 23 | 24 | class OUTLINER_MT_collection(bpy.types.Menu): 25 | bl_label = "Custom Action Collection" 26 | 27 | def draw(self, context): 28 | pass 29 | 30 | # Operator to create the list of sections when right clicking on the property -> Link to property 31 | class OUTLINER_MT_link_mcmenu(bpy.types.Menu): 32 | bl_idname = 'OUTLINER_MT_mc_link_property' 33 | bl_label = 'Link to Property' 34 | 35 | def draw(self, context): 36 | 37 | settings = bpy.context.scene.mc_settings 38 | if settings.em_fixobj: 39 | obj = settings.em_fixobj_pointer 40 | else: 41 | obj = context.active_object 42 | 43 | layout = self.layout 44 | 45 | no_prop = True 46 | for prop in obj.mc_properties: 47 | op = layout.operator(mcops.MC_LinkProperty.bl_idname, text=prop.name, icon=prop.icon) 48 | op.prop_id = prop.id 49 | op.prop_path = prop.path 50 | no_prop = False 51 | 52 | if no_prop: 53 | layout.label(text="No properties found") 54 | 55 | # Operator to create the list of sections when right clicking on the collection -> Add collection to Section 56 | class OUTLINER_MT_collection_mcmenu(bpy.types.Menu): 57 | bl_idname = 'OUTLINER_MT_mc_add_collection' 58 | bl_label = 'Add Collection to Section' 59 | 60 | def draw(self, context): 61 | 62 | settings = bpy.context.scene.mc_settings 63 | if settings.em_fixobj: 64 | obj = settings.em_fixobj_pointer 65 | else: 66 | obj = context.active_object 67 | 68 | layout = self.layout 69 | 70 | no_col_sec = True 71 | for sec in obj.mc_sections: 72 | if sec.type == "COLLECTION": 73 | layout.operator(mcops.MC_AddCollection.bl_idname, text=sec.name, icon=sec.icon).section = sec.name 74 | no_col_sec = False 75 | 76 | if no_col_sec: 77 | layout.label(text="No Collection List sections found") 78 | 79 | def mc_collection_menu(self, context): 80 | self.layout.separator() 81 | self.layout.menu(OUTLINER_MT_collection_mcmenu.bl_idname) 82 | 83 | classes = ( 84 | WM_MT_button_context, 85 | OUTLINER_MT_link_mcmenu, 86 | OUTLINER_MT_collection_mcmenu 87 | ) 88 | 89 | def register(): 90 | for cls in classes: 91 | bpy.utils.register_class(cls) 92 | bpy.types.WM_MT_button_context.append(menu_func) 93 | bpy.types.WM_MT_button_context.append(menu_func_link) 94 | bpy.types.OUTLINER_MT_collection.append(mc_collection_menu) 95 | 96 | def unregister(): 97 | bpy.types.WM_MT_button_context.remove(menu_func) 98 | bpy.types.WM_MT_button_context.remove(menu_func_link) 99 | bpy.types.OUTLINER_MT_collection.remove(mc_collection_menu) 100 | for cls in reversed(classes): 101 | bpy.utils.unregister_class(cls) 102 | -------------------------------------------------------------------------------- /mcops.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import mcdata 3 | from .mcutils import * 4 | from .mccolutils import * 5 | 6 | # Operator to add the right click button on properties 7 | class MC_AddProperty(bpy.types.Operator): 8 | """Add the property to the menu""" 9 | bl_idname = "mc.add_property" 10 | bl_label = "Add property to Menu" 11 | 12 | @classmethod 13 | def poll(cls, context): 14 | return context.active_object is not None 15 | 16 | def execute(self, context): 17 | 18 | settings = bpy.context.scene.mc_settings 19 | if settings.em_fixobj: 20 | obj = settings.em_fixobj_pointer 21 | else: 22 | obj = context.active_object 23 | 24 | #if hasattr(context, 'button_pointer'): 25 | # btn = context.button_pointer 26 | # dump(btn, 'button_pointer') 27 | 28 | if hasattr(context, 'button_prop'): 29 | prop = context.button_prop 30 | #dump(prop, 'button_prop') 31 | 32 | try: 33 | bpy.ops.ui.copy_data_path_button(full_path=True) 34 | except: 35 | self.report({'WARNING'}, 'Menu Creator - Invalid selection.') 36 | return {'FINISHED'} 37 | 38 | rna, path = split_path(context.window_manager.clipboard) 39 | 40 | if obj.mc_enable: 41 | 42 | if mc_add_property_item(obj.mc_properties, [prop.name,rna,path]): 43 | self.report({'INFO'}, 'Menu Creator - Property added to the \'' + obj.name + '\' menu.') 44 | else: 45 | self.report({'WARNING'}, 'Menu Creator - Property of \'' + obj.name + '\' was already added.') 46 | 47 | else: 48 | self.report({'ERROR'}, 'Menu Creator - Can not add property \'' + obj.name + '\'. No menu has been initialized.') 49 | 50 | #if hasattr(context, 'button_operator'): 51 | # op = context.button_operator 52 | # dump(op, 'button_operator') 53 | 54 | return {'FINISHED'} 55 | 56 | # Operator to link a property to another one 57 | class MC_LinkProperty(bpy.types.Operator): 58 | """Link the selected property to this one""" 59 | bl_idname = "mc.link_property" 60 | bl_label = "Link Property" 61 | 62 | prop_id: bpy.props.StringProperty() 63 | prop_path: bpy.props.StringProperty() 64 | 65 | @classmethod 66 | def poll(cls, context): 67 | return context.active_object is not None 68 | 69 | def execute(self, context): 70 | 71 | settings = bpy.context.scene.mc_settings 72 | if settings.em_fixobj: 73 | obj = settings.em_fixobj_pointer 74 | else: 75 | obj = context.active_object 76 | 77 | if hasattr(context, 'button_prop'): 78 | prop = context.button_prop 79 | #dump(prop, 'button_prop') 80 | 81 | try: 82 | bpy.ops.ui.copy_data_path_button(full_path=True) 83 | except: 84 | self.report({'WARNING'}, 'Menu Creator - Invalid selection.') 85 | return {'FINISHED'} 86 | 87 | rna, path = split_path(context.window_manager.clipboard) 88 | 89 | if obj.mc_enable: 90 | 91 | i = mc_find_index(obj.mc_properties, ['',self.prop_path,self.prop_id]) 92 | 93 | prop_type = type(eval(obj.mc_properties[i].path + '.' + obj.mc_properties[i].id)) 94 | if '].[' in rna + '.' + path: 95 | link_type = type(eval(rna + path)) 96 | else: 97 | link_type = type(eval(rna + '.' + path)) 98 | 99 | if prop_type == link_type: 100 | 101 | already_added = False 102 | for el in obj.mc_properties[i].linked_props: 103 | if el.path == rna and el.id == path: 104 | already_added = True 105 | break 106 | if not already_added: 107 | add_item = obj.mc_properties[i].linked_props.add() 108 | add_item.id = path 109 | add_item.path = rna 110 | 111 | self.report({'INFO'}, 'Menu Creator - Property \'' + path + '\' linked to \'' + obj.mc_properties[i].name + '\'') 112 | else: 113 | self.report({'WARNING'}, 'Menu Creator - Property \'' + path + '\' already linked to \'' + obj.mc_properties[i].name + '\'') 114 | 115 | else: 116 | self.report({'ERROR'}, 'Menu Creator - Property \'' + path + '\' can not be linked to \'' + obj.mc_properties[i].name + '\'') 117 | if settings.ms_debug: 118 | print('MenuCreator - Property \'' + path + '\' can not be linked to \'' + obj.mc_properties[i].name + '\'') 119 | print(' Data types are ' + str(link_type) + ' and ' + str(prop_type) + '.') 120 | 121 | else: 122 | self.report({'ERROR'}, 'Menu Creator - Can not link property in \'' + obj.name + '\'. No menu has been initialized.') 123 | 124 | return {'FINISHED'} 125 | 126 | # Operator to add the collection to the selected section 127 | class MC_AddCollection(bpy.types.Operator): 128 | """Add the collection to the selected section""" 129 | bl_idname = "mc.add_collection" 130 | bl_label = "Add collection to Menu" 131 | 132 | section: bpy.props.StringProperty() 133 | 134 | @classmethod 135 | def poll(cls, context): 136 | return context.active_object is not None 137 | 138 | def execute(self, context): 139 | 140 | settings = bpy.context.scene.mc_settings 141 | if settings.em_fixobj: 142 | obj = settings.em_fixobj_pointer 143 | else: 144 | obj = context.active_object 145 | 146 | add_coll = bpy.context.collection 147 | 148 | sec_index = mc_find_index_section(obj.mc_sections, self.section) 149 | 150 | i=True 151 | for el in obj.mc_sections[sec_index].collections: 152 | if el.collection == add_coll: 153 | i=False 154 | break 155 | if i: 156 | add_item = obj.mc_sections[sec_index].collections.add() 157 | add_item.collection = add_coll 158 | self.report({'INFO'}, 'Menu Creator - Collection has been added to section \''+self.section+'\'.') 159 | else: 160 | self.report({'WARNING'}, 'Menu Creator - Collection was already added to section \''+self.section+'\'.') 161 | 162 | return {'FINISHED'} 163 | 164 | # Operator to clean all properties and sections from all objects 165 | class MC_CleanAll(bpy.types.Operator): 166 | """Clean all the menus.\nIf you choose reset, it will also delete all Menu options from all objects""" 167 | bl_idname = "mc.cleanprop" 168 | bl_label = "Clean all the properties" 169 | 170 | reset : bpy.props.BoolProperty(default=False) 171 | 172 | def execute(self, context): 173 | 174 | mc_clean_properties() 175 | mc_clean_sections() 176 | 177 | if self.reset: 178 | for obj in bpy.data.objects: 179 | obj.mc_enable = False 180 | 181 | self.report({'INFO'}, 'Menu Creator - All the objects has been reset.') 182 | 183 | return {'FINISHED'} 184 | 185 | # Operator to clean all properties and sections from an objects. If reset is on, it will also disable the menu for that object 186 | class MC_CleanObject(bpy.types.Operator): 187 | """Clean all the object properties.\nIf you choose reset, it will also delete all Menu options from the object""" 188 | bl_idname = "mc.cleanpropobj" 189 | bl_label = "Clean the object" 190 | 191 | reset : bpy.props.BoolProperty(default=False) 192 | 193 | def execute(self, context): 194 | 195 | settings = bpy.context.scene.mc_settings 196 | if settings.em_fixobj: 197 | obj = settings.em_fixobj_pointer 198 | else: 199 | obj = context.active_object 200 | 201 | mc_clean_single_properties(obj) 202 | mc_clean_single_sections(obj) 203 | if self.reset: 204 | obj.mc_enable = False 205 | 206 | self.report({'INFO'}, 'Menu Creator - \'' + obj.name + '\' menu has been reset.') 207 | 208 | return {'FINISHED'} 209 | 210 | # Operator to remove a linked property (button in UI) 211 | class MC_RemoveLinkedProperty(bpy.types.Operator): 212 | """Remove the linked property""" 213 | bl_idname = "mc.removelinkedproperty" 214 | bl_label = "" 215 | 216 | prop_index : bpy.props.IntProperty() 217 | link_path : bpy.props.StringProperty() 218 | link_id : bpy.props.StringProperty() 219 | 220 | @classmethod 221 | def poll(cls, context): 222 | return context.active_object is not None 223 | 224 | def execute(self, context): 225 | 226 | settings = bpy.context.scene.mc_settings 227 | if settings.em_fixobj: 228 | obj = settings.em_fixobj_pointer 229 | else: 230 | obj = context.active_object 231 | props = obj.mc_properties 232 | 233 | i=-1 234 | for el in obj.mc_properties[self.prop_index].linked_props: 235 | i=i+1 236 | if el.path == self.link_path and el.id == self.link_id: 237 | break 238 | if i>=0: 239 | obj.mc_properties[self.prop_index].linked_props.remove(i) 240 | 241 | return {'FINISHED'} 242 | 243 | # Single Property settings 244 | class MC_PropertySettings(bpy.types.Operator): 245 | """Modify some of the property settings""" 246 | bl_idname = "mc.propsettings" 247 | bl_label = "Property settings" 248 | bl_icon = "PREFERENCES" 249 | bl_options = {'UNDO'} 250 | 251 | name : bpy.props.StringProperty(name='Name', 252 | description="Choose the name of the property") 253 | path : bpy.props.StringProperty() 254 | id : bpy.props.StringProperty() 255 | icon : bpy.props.EnumProperty(name='Icon', 256 | description="Choose the icon.\nNote that the icon name MUST respect Blender convention. All the icons can be found in the Icon Viewer default Blender addon.",items=mcdata.mc_icon_list) 257 | section : bpy.props.EnumProperty(name='Section', 258 | description="Choose the section of the property",items=mc_section_list) 259 | 260 | def execute(self, context): 261 | 262 | settings = bpy.context.scene.mc_settings 263 | if settings.em_fixobj: 264 | obj = settings.em_fixobj_pointer 265 | else: 266 | obj = context.active_object 267 | 268 | i = mc_find_index(obj.mc_properties,[self.name,self.path,self.id]) 269 | 270 | if i>=0: 271 | obj.mc_properties[i].name = self.name 272 | obj.mc_properties[i].icon = self.icon 273 | obj.mc_properties[i].section = self.section 274 | 275 | return {'FINISHED'} 276 | 277 | def invoke(self, context, event): 278 | 279 | settings = bpy.context.scene.mc_settings 280 | 281 | if settings.ms_debug: 282 | return context.window_manager.invoke_props_dialog(self, width=650) 283 | else: 284 | return context.window_manager.invoke_props_dialog(self, width=550) 285 | 286 | def draw(self, context): 287 | 288 | settings = bpy.context.scene.mc_settings 289 | if settings.em_fixobj: 290 | obj = settings.em_fixobj_pointer 291 | else: 292 | obj = context.active_object 293 | 294 | i = mc_find_index(obj.mc_properties,[self.name,self.path,self.id]) 295 | 296 | layout = self.layout 297 | 298 | layout.prop(self, "name") 299 | layout.prop(self, "icon") 300 | layout.prop(self, "section") 301 | 302 | layout.separator() 303 | layout.label(text="Property info", icon="INFO") 304 | box = layout.box() 305 | box.label(text="Identifier: "+self.id) 306 | 307 | if settings.ms_debug: 308 | layout.label(text="Full path", icon="RNA") 309 | box = layout.box() 310 | box.label(text=self.path+'.'+self.id) 311 | 312 | if len(obj.mc_properties[i].linked_props)>0: 313 | layout.separator() 314 | layout.label(text="Linked Properties", icon="LINKED") 315 | box = layout.box() 316 | for prop in obj.mc_properties[i].linked_props: 317 | row = box.row() 318 | row.label(text=prop.path + '.' + prop.id, icon="DOT") 319 | link_del_op = row.operator(MC_RemoveLinkedProperty.bl_idname, icon="X") 320 | link_del_op.prop_index = i 321 | link_del_op.link_id = prop.id 322 | link_del_op.link_path = prop.path 323 | 324 | 325 | # Swap Properties Operator 326 | class MC_SwapProperty(bpy.types.Operator): 327 | """Change the position of the property""" 328 | bl_idname = "mc.swapprops" 329 | bl_label = "Change the property position" 330 | 331 | mod : bpy.props.BoolProperty(default=False) # False = down, True = Up 332 | 333 | name : bpy.props.StringProperty() 334 | path : bpy.props.StringProperty() 335 | id : bpy.props.StringProperty() 336 | 337 | def execute(self, context): 338 | 339 | settings = bpy.context.scene.mc_settings 340 | if settings.em_fixobj: 341 | obj = settings.em_fixobj_pointer 342 | else: 343 | obj = context.active_object 344 | col = sorted(obj.mc_properties, key = mc_prop_ID) 345 | col_len = mc_len_collection(col) 346 | 347 | i = mc_find_index(col,[self.name,self.path,self.id]) 348 | 349 | if i>=0: 350 | if self.mod: 351 | 352 | j=i 353 | while j>0: 354 | j = j - 1 355 | if col[j].section==col[i].section: 356 | break 357 | if j>-1: 358 | 359 | col[i].mc_id = j 360 | col[j].mc_id = i 361 | 362 | else: 363 | 364 | j=i 365 | while j=0: 539 | 540 | for el in prop_obj: 541 | if el.section == self.name: 542 | el.section = self.name_edit 543 | 544 | sec_obj[i].name = self.name_edit 545 | sec_obj[i].icon = self.icon 546 | sec_obj[i].collapsable = self.collapsable 547 | sec_obj[i].collections_enable_global_smoothcorrection = self.collections_enable_global_smoothcorrection 548 | sec_obj[i].collections_enable_global_shrinkwrap = self.collections_enable_global_shrinkwrap 549 | sec_obj[i].collections_enable_global_mask = self.collections_enable_global_mask 550 | sec_obj[i].collections_enable_global_normalautosmooth = self.collections_enable_global_normalautosmooth 551 | sec_obj[i].outfit_enable = self.outfit_enable 552 | if obj.type == "MESH": 553 | sec_obj[i].outfit_body = obj 554 | 555 | return {'FINISHED'} 556 | 557 | def invoke(self, context, event): 558 | 559 | settings = bpy.context.scene.mc_settings 560 | 561 | if settings.em_fixobj: 562 | obj = settings.em_fixobj_pointer 563 | else: 564 | obj = context.active_object 565 | sec_obj = obj.mc_sections 566 | 567 | self.name_edit = self.name 568 | self.ID = mc_find_index_section(sec_obj,self.name) 569 | self.collapsable = sec_obj[self.ID].collapsable 570 | self.collections_enable_global_smoothcorrection = sec_obj[self.ID].collections_enable_global_smoothcorrection 571 | self.collections_enable_global_shrinkwrap = sec_obj[self.ID].collections_enable_global_shrinkwrap 572 | self.collections_enable_global_mask = sec_obj[self.ID].collections_enable_global_mask 573 | self.collections_enable_global_normalautosmooth = sec_obj[self.ID].collections_enable_global_normalautosmooth 574 | self.outfit_enable = sec_obj[self.ID].outfit_enable 575 | 576 | return context.window_manager.invoke_props_dialog(self) 577 | 578 | def draw(self, context): 579 | 580 | settings = bpy.context.scene.mc_settings 581 | 582 | if settings.em_fixobj: 583 | obj = settings.em_fixobj_pointer 584 | else: 585 | obj = context.active_object 586 | sec_obj = obj.mc_sections 587 | 588 | scale = 3.0 589 | 590 | layout = self.layout 591 | 592 | row=layout.row() 593 | row.label(text="Name:") 594 | row.scale_x=scale 595 | row.prop(self, "name_edit", text="") 596 | 597 | row=layout.row() 598 | row.label(text="Icon:") 599 | row.scale_x=scale 600 | row.prop(self, "icon", text="") 601 | 602 | row=layout.row() 603 | row.label(text="") 604 | row.scale_x=scale 605 | row.prop(self, "collapsable") 606 | 607 | layout.separator() 608 | col = layout.column() 609 | col.enabled = False 610 | col.prop(self, "type") 611 | if self.type == "COLLECTION": 612 | layout.separator() 613 | row = layout.row() 614 | row.label(text="") 615 | row.scale_x = 3 616 | row.prop(self,"collections_enable_global_smoothcorrection") 617 | row = layout.row() 618 | row.label(text="") 619 | row.scale_x = 3 620 | row.prop(self,"collections_enable_global_shrinkwrap") 621 | row = layout.row() 622 | row.label(text="") 623 | row.scale_x = 3 624 | row.prop(self,"collections_enable_global_mask") 625 | row = layout.row() 626 | row.label(text="") 627 | row.scale_x = 3 628 | row.prop(self,"collections_enable_global_normalautosmooth") 629 | layout.separator() 630 | row = layout.row() 631 | row.label(text="") 632 | row.scale_x = 3 633 | row.prop(self,"outfit_enable") 634 | 635 | # Operator to change Section position 636 | class MC_SwapSection(bpy.types.Operator): 637 | """Change the position of the section""" 638 | bl_idname = "mc.swapsections" 639 | bl_label = "Change the section position" 640 | 641 | mod : bpy.props.BoolProperty(default=False) # False = down, True = Up 642 | 643 | name : bpy.props.StringProperty() 644 | icon : bpy.props.StringProperty() 645 | 646 | def execute(self, context): 647 | 648 | settings = bpy.context.scene.mc_settings 649 | if settings.em_fixobj: 650 | obj = settings.em_fixobj_pointer 651 | else: 652 | obj = context.active_object 653 | col = obj.mc_sections 654 | col_len = mc_len_collection(col) 655 | 656 | sec_index = mc_find_index_section(col,self.name) 657 | i = col[sec_index].id 658 | 659 | if self.mod and i > 1: 660 | j = mc_find_index_section_fromID(col, i-1) 661 | col[sec_index].id = i-1 662 | col[j].id = i 663 | elif not self.mod and i < col_len-1: 664 | j = mc_find_index_section_fromID(col, i+1) 665 | col[sec_index].id = i+1 666 | col[j].id = i 667 | 668 | return {'FINISHED'} 669 | 670 | # Delete Section 671 | class MC_DeleteSection(bpy.types.Operator): 672 | """Delete Section""" 673 | bl_idname = "mc.deletesection" 674 | bl_label = "Section settings" 675 | bl_options = {'UNDO'} 676 | 677 | name : bpy.props.StringProperty(name='Name', 678 | description="Choose the name of the section") 679 | 680 | def execute(self, context): 681 | 682 | settings = bpy.context.scene.mc_settings 683 | if settings.em_fixobj: 684 | obj = settings.em_fixobj_pointer 685 | else: 686 | obj = context.active_object 687 | sec_obj = obj.mc_sections 688 | 689 | i=-1 690 | for el in sec_obj: 691 | i=i+1 692 | if el.name == self.name: 693 | break 694 | 695 | if i>=0: 696 | 697 | j = sec_obj[i].id 698 | 699 | for k in range(j+1,len(sec_obj)): 700 | sec_obj[mc_find_index_section_fromID(sec_obj, k)].id = k-1 701 | 702 | sec_obj.remove(i) 703 | 704 | self.report({'INFO'}, 'Menu Creator - Section \'' + self.name +'\' deleted.') 705 | 706 | return {'FINISHED'} 707 | 708 | # Operator to shiwtch visibility of an object 709 | class MC_CollectionObjectVisibility(bpy.types.Operator): 710 | """Chenge the visibility of the selected object""" 711 | bl_idname = "mc.colobjvisibility" 712 | bl_label = "Hide/Unhide Object visibility" 713 | bl_options = {'UNDO'} 714 | 715 | obj : bpy.props.StringProperty() 716 | sec : bpy.props.StringProperty() 717 | 718 | def execute(self, context): 719 | 720 | bpy.data.objects[self.obj].hide_viewport = not bpy.data.objects[self.obj].hide_viewport 721 | bpy.data.objects[self.obj].hide_render = not bpy.data.objects[self.obj].hide_render 722 | 723 | settings = bpy.context.scene.mc_settings 724 | if settings.em_fixobj: 725 | body_obj = settings.em_fixobj_pointer 726 | else: 727 | body_obj = context.active_object 728 | sec_obj = body_obj.mc_sections 729 | i = mc_find_index_section(sec_obj,self.sec) 730 | 731 | if sec_obj[i].outfit_enable: 732 | if sec_obj[i].outfit_body: 733 | for modifier in sec_obj[i].outfit_body.modifiers: 734 | if modifier.type == "MASK" and self.obj in modifier.name and sec_obj[i].collections_global_mask: 735 | modifier.show_viewport = not bpy.data.objects[self.obj].hide_viewport 736 | modifier.show_render = not bpy.data.objects[self.obj].hide_viewport 737 | else: 738 | self.report({'WARNING'}, 'Menu Creator - Outfit Body has not been specified.') 739 | 740 | return {'FINISHED'} 741 | 742 | # Operator to delete a collection 743 | class MC_RemoveCollection(bpy.types.Operator): 744 | """Remove the selected collection from the Menu.\nThe collection will NOT be deleted""" 745 | bl_idname = "mc.deletecollection" 746 | bl_label = "Remove the selected collection from the menu" 747 | bl_options = {'UNDO'} 748 | 749 | col : bpy.props.StringProperty() 750 | sec : bpy.props.StringProperty() 751 | 752 | def execute(self, context): 753 | 754 | settings = bpy.context.scene.mc_settings 755 | if settings.em_fixobj: 756 | obj = settings.em_fixobj_pointer 757 | else: 758 | obj = context.active_object 759 | sec_obj = obj.mc_sections 760 | 761 | sec_index = mc_find_index_section(sec_obj,self.sec) 762 | 763 | i = 0 764 | for el in sec_obj[sec_index].collections: 765 | if el.collection.name == self.col: 766 | sec_obj[sec_index].collections.remove(i) 767 | break 768 | i = i + 1 769 | 770 | self.report({'INFO'}, 'Menu Creator - Collection removed from the Menu.') 771 | 772 | return {'FINISHED'} 773 | 774 | # Initial Configuration Operator 775 | class MC_InitialConfiguration(bpy.types.Operator): 776 | """Clean all the object properties""" 777 | bl_idname = "mc.initialconfig" 778 | bl_label = "Clean all the properties" 779 | 780 | def execute(self, context): 781 | 782 | settings = bpy.context.scene.mc_settings 783 | if settings.em_fixobj: 784 | obj = settings.em_fixobj_pointer 785 | else: 786 | obj = context.active_object 787 | 788 | mc_clean_single_sections(obj) 789 | mc_clean_single_properties(obj) 790 | 791 | add_item = obj.mc_sections.add() 792 | add_item.id = 0 793 | add_item.name = "Unsorted" 794 | add_item.icon = "LIBRARY_DATA_BROKEN" 795 | 796 | obj.mc_enable = True 797 | 798 | self.report({'INFO'}, 'Menu Creator - Menu for \''+obj.name+'\' successfully created.') 799 | 800 | return {'FINISHED'} 801 | 802 | classes = ( 803 | MC_AddProperty, 804 | MC_LinkProperty, 805 | MC_AddCollection, 806 | MC_CleanAll, 807 | MC_CleanObject, 808 | MC_RemoveLinkedProperty, 809 | MC_PropertySettings, 810 | MC_SwapProperty, 811 | MC_RemoveProperty, 812 | MC_AddSection, 813 | MC_SectionSettings, 814 | MC_SwapSection, 815 | MC_DeleteSection, 816 | MC_CollectionObjectVisibility, 817 | MC_RemoveCollection, 818 | MC_InitialConfiguration 819 | ) 820 | 821 | def register(): 822 | for cls in classes: 823 | bpy.utils.register_class(cls) 824 | 825 | def unregister(): 826 | for cls in reversed(classes): 827 | bpy.utils.unregister_class(cls) -------------------------------------------------------------------------------- /mcpanels.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .mccolutils import * 3 | from .mcutils import * 4 | 5 | # Poll functions 6 | 7 | @classmethod 8 | def mc_panel_poll(cls, context): 9 | 10 | settings = bpy.context.scene.mc_settings 11 | if settings.em_fixobj: 12 | obj = settings.em_fixobj_pointer 13 | else: 14 | obj = context.active_object 15 | 16 | return obj.mc_enable 17 | 18 | # User Interface Panels 19 | 20 | class MainPanel: 21 | bl_space_type = "VIEW_3D" 22 | bl_region_type = "UI" 23 | bl_category = "Menu" 24 | 25 | class OBJECT_PT_MenuCreator_InitialConfiguration_Panel(MainPanel, bpy.types.Panel): 26 | bl_idname = "OBJECT_PT_MenuCreator_InitialConfiguration_Panel" 27 | bl_label = "Initial Configuration" 28 | 29 | @classmethod 30 | def poll(cls, context): 31 | 32 | settings = bpy.context.scene.mc_settings 33 | if settings.em_fixobj: 34 | obj = settings.em_fixobj_pointer 35 | else: 36 | obj = context.active_object 37 | 38 | if obj is not None: 39 | return not obj.mc_enable 40 | else: 41 | return False 42 | 43 | def draw(self, context): 44 | 45 | layout = self.layout 46 | 47 | layout.label(text="Menu Configuration") 48 | 49 | layout.operator('mc.initialconfig', text="Create Menu") 50 | 51 | class OBJECT_PT_MenuCreator_Panel(MainPanel, bpy.types.Panel): 52 | bl_idname = "OBJECT_PT_MenuCreator_Panel" 53 | bl_label = "Menu" 54 | 55 | @classmethod 56 | def poll(cls, context): 57 | 58 | settings = bpy.context.scene.mc_settings 59 | if settings.em_fixobj: 60 | obj = settings.em_fixobj_pointer 61 | else: 62 | obj = context.active_object 63 | 64 | if obj is not None: 65 | return obj.mc_enable 66 | else: 67 | return False 68 | 69 | def draw(self, context): 70 | 71 | settings = bpy.context.scene.mc_settings 72 | if settings.em_fixobj: 73 | obj = settings.em_fixobj_pointer 74 | else: 75 | obj = context.active_object 76 | mc_col = obj.mc_properties 77 | mcs_col = obj.mc_sections 78 | mc_col_len = mc_len_collection(mc_col) 79 | mcs_col_len = mc_len_collection(mcs_col) 80 | 81 | layout = self.layout 82 | 83 | row = layout.row(align=False) 84 | menu_name = settings.mss_name; 85 | if settings.mss_obj_name: 86 | menu_name = menu_name+obj.name 87 | row.label(text=menu_name) 88 | 89 | if settings.ms_editmode: 90 | row.prop(obj, "mc_edit_enable", text="",icon="MODIFIER") 91 | row.operator("mc.addsection",text="",icon="ADD") 92 | if settings.em_fixobj: 93 | row.prop(settings,"em_fixobj",icon="PINNED", text="") 94 | else: 95 | row.prop(settings,"em_fixobj",icon="UNPINNED", text= "") 96 | else: 97 | if settings.em_fixobj: 98 | row.prop(settings,"em_fixobj",icon="PINNED", text="") 99 | else: 100 | row.prop(settings,"em_fixobj",icon="UNPINNED", text= "") 101 | 102 | if mcs_col_len>1: 103 | 104 | for sec in sorted(mcs_col, key = mc_sec_ID): 105 | 106 | if sec.type == "DEFAULT": 107 | 108 | sec_empty = True 109 | sec_hidden = True 110 | for el in mc_col: 111 | if el.section == sec.name: 112 | sec_empty = False 113 | if not el.hide: 114 | sec_hidden = False 115 | 116 | if (sec_empty and sec.name == "Unsorted") or (not obj.mc_edit_enable and not sec_empty and sec_hidden): 117 | continue 118 | else: 119 | row = layout.row(align=False) 120 | if sec.collapsable: 121 | row.prop(sec, "collapsed", icon="TRIA_DOWN" if not sec.collapsed else "TRIA_RIGHT", icon_only=True, emboss=False) 122 | if sec.icon == "NONE": 123 | row.label(text=sec.name) 124 | else: 125 | row.label(text=sec.name,icon=sec.icon) 126 | 127 | if obj.mc_edit_enable: 128 | 129 | if sec.name != "Unsorted": 130 | ssett_button = row.operator("mc.sectionsettings", icon="PREFERENCES", text="") 131 | ssett_button.name = sec.name 132 | ssett_button.icon = sec.icon 133 | ssett_button.type = sec.type 134 | 135 | row2 = row.row(align=True) 136 | sup_button = row2.operator("mc.swapsections", icon="TRIA_UP", text="") 137 | sup_button.mod = True 138 | sup_button.name = sec.name 139 | sup_button.icon = sec.icon 140 | sdown_button = row2.operator("mc.swapsections", icon="TRIA_DOWN", text="") 141 | sdown_button.mod = False 142 | sdown_button.name = sec.name 143 | sdown_button.icon = sec.icon 144 | 145 | if not sec.collapsed: 146 | box = layout.box() 147 | if sec_empty and sec.name != "Unsorted": 148 | row = box.row(align=False) 149 | row.label(text="Section Empty", icon="ERROR") 150 | row.operator("mc.deletesection",text="",icon="X").name = sec.name 151 | 152 | if not sec.collapsed: 153 | 154 | for el in sorted(mc_col, key = mc_prop_ID): 155 | 156 | if el.section == sec.name: 157 | 158 | el_index = mc_find_index(mc_col,[el.name,el.path,el.id]) 159 | 160 | if obj.mc_edit_enable: 161 | 162 | row = box.row(align=False) 163 | if el.icon !="NONE": 164 | row.label(text=el.name,icon=el.icon) 165 | else: 166 | row.label(text=el.name) 167 | 168 | sett_button = row.operator("mc.propsettings", icon="PREFERENCES", text="") 169 | sett_button.name = el.name 170 | sett_button.path = el.path 171 | sett_button.id = el.id 172 | sett_button.icon = el.icon 173 | sett_button.section = el.section 174 | 175 | row2 = row.row(align=True) 176 | up_button = row2.operator("mc.swapprops", icon="TRIA_UP", text="") 177 | up_button.mod = True 178 | up_button.name = el.name 179 | up_button.path = el.path 180 | up_button.id = el.id 181 | down_button = row2.operator("mc.swapprops", icon="TRIA_DOWN", text="") 182 | down_button.mod = False 183 | down_button.name = el.name 184 | down_button.path = el.path 185 | down_button.id = el.id 186 | 187 | if el.hide: 188 | row.prop(el, "hide", text="", icon = "HIDE_ON") 189 | else: 190 | row.prop(el, "hide", text="", icon = "HIDE_OFF") 191 | 192 | del_button = row.operator("mc.removeproperty", icon="X", text="") 193 | del_button.path = el.path 194 | del_button.id = el.id 195 | else: 196 | 197 | if not el.hide: 198 | row = box.row(align=False) 199 | if el.icon !="NONE": 200 | row.label(text=el.name,icon=el.icon) 201 | else: 202 | row.label(text=el.name) 203 | 204 | row.scale_x=1.0 205 | row.prop(eval(el.path), el.id, text="") 206 | 207 | elif sec.type == "COLLECTION": 208 | 209 | sec_empty = True 210 | for el in sec.collections: 211 | sec_empty = False 212 | break 213 | 214 | row = layout.row(align=False) 215 | if sec.collapsable: 216 | row.prop(sec, "collapsed", icon="TRIA_DOWN" if not sec.collapsed else "TRIA_RIGHT", icon_only=True, emboss=False) 217 | if sec.icon == "NONE": 218 | row.label(text=sec.name) 219 | else: 220 | row.label(text=sec.name,icon=sec.icon) 221 | 222 | if obj.mc_edit_enable: 223 | 224 | ssett_button = row.operator("mc.sectionsettings", icon="PREFERENCES", text="") 225 | ssett_button.name = sec.name 226 | ssett_button.icon = sec.icon 227 | ssett_button.type = sec.type 228 | 229 | row2 = row.row(align=True) 230 | sup_button = row2.operator("mc.swapsections", icon="TRIA_UP", text="") 231 | sup_button.mod = True 232 | sup_button.name = sec.name 233 | sup_button.icon = sec.icon 234 | sdown_button = row2.operator("mc.swapsections", icon="TRIA_DOWN", text="") 235 | sdown_button.mod = False 236 | sdown_button.name = sec.name 237 | sdown_button.icon = sec.icon 238 | 239 | row.operator("mc.deletesection",text="",icon="X").name = sec.name 240 | 241 | if not sec.collapsed and len(sec.collections)>0: 242 | box = layout.box() 243 | if sec.outfit_enable: 244 | box.prop(sec,"outfit_body", text="Body", icon="OUTLINER_OB_MESH") 245 | 246 | if len(sec.collections)>0: 247 | box.label(text="Collection List", icon="OUTLINER_COLLECTION") 248 | box = box.box() 249 | for collection in sec.collections: 250 | row = box.row() 251 | row.label(text=collection.collection.name) 252 | del_col = row.operator("mc.deletecollection",text="",icon="X") 253 | del_col.sec = sec.name 254 | del_col.col = collection.collection.name 255 | 256 | else: 257 | if not sec.collapsed: 258 | box = layout.box() 259 | if sec_empty: 260 | row = box.row(align=False) 261 | row.label(text="No Collection Assigned", icon="ERROR") 262 | row.operator("mc.deletesection",text="",icon="X").name = sec.name 263 | 264 | if len(sec.collections)>0: 265 | box.prop(sec,"collections_list", text="") 266 | box2 = box.box() 267 | if len(bpy.data.collections[sec.collections_list].objects)>0: 268 | for obj2 in bpy.data.collections[sec.collections_list].objects: 269 | row = box2.row() 270 | if obj2.hide_viewport: 271 | vop=row.operator("mc.colobjvisibility",text=obj2.name, icon='OUTLINER_OB_'+obj2.type) 272 | vop.obj = obj2.name 273 | vop.sec = sec.name 274 | else: 275 | vop = row.operator("mc.colobjvisibility",text=obj2.name, icon='OUTLINER_OB_'+obj2.type, depress = True) 276 | vop.obj = obj2.name 277 | vop.sec = sec.name 278 | else: 279 | box2.label(text="This Collection seems empty", icon="ERROR") 280 | 281 | if sec.collections_enable_global_smoothcorrection or sec.collections_enable_global_shrinkwrap or sec.collections_enable_global_mask or sec.collections_enable_global_normalautosmooth: 282 | box.label(text= "Global Properties", icon="MODIFIER") 283 | box2 = box.box() 284 | if sec.collections_enable_global_smoothcorrection: 285 | box2.prop(sec,"collections_global_smoothcorrection") 286 | if sec.collections_enable_global_shrinkwrap: 287 | box2.prop(sec,"collections_global_shrinkwrap") 288 | if sec.collections_enable_global_mask: 289 | box2.prop(sec,"collections_global_mask") 290 | if sec.collections_enable_global_normalautosmooth: 291 | box2.prop(sec,"collections_global_normalautosmooth") 292 | 293 | else: 294 | box = layout.box() 295 | box.label(text="No section added.",icon="ERROR") 296 | 297 | 298 | class OBJECT_PT_MenuCreator_Settings_Panel(MainPanel, bpy.types.Panel): 299 | bl_idname = "OBJECT_PT_MenuCreator_Settings_Panel" 300 | bl_label = "Settings" 301 | 302 | def draw(self, context): 303 | 304 | settings = bpy.context.scene.mc_settings 305 | 306 | layout = self.layout 307 | 308 | # Main Settings 309 | layout.label(text="Main Settings",icon="SETTINGS") 310 | box = layout.box() 311 | 312 | box.prop(settings,"ms_editmode") 313 | box.prop(settings,"ms_debug") 314 | box.prop(settings,"ms_advanced") 315 | 316 | # Menu specific settings 317 | layout.label(text="Menu Settings",icon="SETTINGS") 318 | box = layout.box() 319 | 320 | box.prop(settings,"mss_name") 321 | box.prop(settings,"mss_obj_name") 322 | 323 | layout.label(text="Reset functions",icon="SETTINGS") 324 | box = layout.box() 325 | 326 | box.operator('mc.cleanpropobj', text="Reset Object", icon="ERROR").reset = True 327 | box.operator('mc.cleanprop', text="Reset All Objects", icon="ERROR").reset = True 328 | 329 | classes = ( 330 | OBJECT_PT_MenuCreator_InitialConfiguration_Panel, 331 | OBJECT_PT_MenuCreator_Panel, 332 | OBJECT_PT_MenuCreator_Settings_Panel 333 | ) 334 | 335 | def register(): 336 | for cls in classes: 337 | bpy.utils.register_class(cls) 338 | 339 | def unregister(): 340 | for cls in reversed(classes): 341 | bpy.utils.unregister_class(cls) -------------------------------------------------------------------------------- /mcsettings.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | # Class with all the settings variables 4 | class MC_Settings(bpy.types.PropertyGroup): 5 | 6 | # Update functions for settings 7 | # Function to avoid edit mode and fixed object while exiting edit mode 8 | def mc_ms_editmode_update(self, context): 9 | 10 | if not self.ms_editmode: 11 | for obj in bpy.data.objects: 12 | obj.mc_edit_enable = False 13 | 14 | return 15 | 16 | # Function to save the fixed object pointer to be used until the object is released 17 | def mc_em_fixobj_update(self, context): 18 | 19 | if self.em_fixobj: 20 | self.em_fixobj_pointer = context.active_object 21 | 22 | return 23 | 24 | # Main Settings definitions 25 | ms_editmode: bpy.props.BoolProperty(name="Enable Edit Mode Tools", 26 | description="Unlock tools to customize the menu.\nDisable when the Menu is complete", 27 | default=False, 28 | update = mc_ms_editmode_update) 29 | ms_advanced: bpy.props.BoolProperty(name="Advanced Options", 30 | description="Unlock advanced options", 31 | default=False) 32 | ms_debug: bpy.props.BoolProperty(name="Debug mode", 33 | description="Unlock debug mode.\nMore messaged will be generated in the console.\nEnable it only if you encounter problems, as it might degrade general Blender performance", 34 | default=False) 35 | 36 | # Menu Specific properties 37 | mss_name: bpy.props.StringProperty(name="Name", 38 | description="Name of the menu.\nChoose the name of the menu to be shown before the properties", 39 | default="Object: ") 40 | mss_obj_name: bpy.props.BoolProperty(name="Show the Object Name", 41 | description="Show the Object name after the Name.\nFor instance, if the Name is \"Object: \", the shown name will be \"Object: name_of_object\"", 42 | default=True) 43 | 44 | # Edit mode properties 45 | em_fixobj: bpy.props.BoolProperty(name="Pin Object", 46 | description="Pin the Object you are using to edit the menu.\nThe object you pin will be considered as the target of all properties addition, and only this Object menu will be shown", 47 | default=False, 48 | update = mc_em_fixobj_update) 49 | em_fixobj_pointer : bpy.props.PointerProperty(type=bpy.types.Object) 50 | 51 | def register(): 52 | bpy.utils.register_class(MC_Settings) 53 | bpy.types.Scene.mc_settings = bpy.props.PointerProperty(type=MC_Settings) 54 | 55 | # Object specific properties 56 | bpy.types.Object.mc_enable = bpy.props.BoolProperty(name="", default=False) 57 | bpy.types.Object.mc_edit_enable = bpy.props.BoolProperty(name="Edit Mode", default=False, description="Enable edit mode in this menu.\nActivating this option you will have access to various tools to modify properties and sections") 58 | 59 | def unregister(): 60 | del bpy.types.Object.mc_edit_enable 61 | del bpy.types.Object.mc_enable 62 | del bpy.types.Scene.mc_settings 63 | bpy.utils.unregister_class(MC_Settings) 64 | -------------------------------------------------------------------------------- /mcutils.py: -------------------------------------------------------------------------------- 1 | # Right click functions and operators 2 | def dump(obj, text): 3 | print('-'*40, text, '-'*40) 4 | for attr in dir(obj): 5 | if hasattr( obj, attr ): 6 | print( "obj.%s = %s" % (attr, getattr(obj, attr))) 7 | 8 | def split_path(str): 9 | """Splits a data path into rna + id. 10 | 11 | This is the core of creating controls for properties. 12 | """ 13 | # First split the last part of the dot-chain 14 | rna, path = str.rsplit('.',1) 15 | # If the last part contains a '][', it's a custom property for a collection item 16 | if '][' in path: 17 | # path is 'somecol["itemkey"]["property"]' 18 | path, rem = path.rsplit('[', 1) 19 | # path is 'somecol["itemkey"]' ; rem is '"property"]' 20 | rna = rna + '.' + path 21 | # rna is 'rna.path.to.somecol["itemkey"]' 22 | path = '[' + rem 23 | # path is '["property"]' 24 | # If the last part only contains a single '[', 25 | # it's an indexed value 26 | elif '[' in path: 27 | # path is 'someprop[n]' 28 | path, rem = path.rsplit('[', 1) 29 | # path is 'someprop' ; rem is ignored 30 | return rna, path 31 | --------------------------------------------------------------------------------