├── LICENSE.txt ├── README.md ├── __init__.py ├── config.py ├── ops ├── __init__.py ├── direct_use_ops.py ├── inspect_ops.py ├── main_ops.py ├── missing_file_ops.py ├── support_me_ops.py └── utils │ ├── clean.py │ ├── delete.py │ ├── duplicate.py │ └── nuke.py ├── stats ├── count.py ├── misc.py ├── missing.py ├── unnamed.py ├── unused.py └── users.py ├── ui ├── __init__.py ├── inspect_ui.py ├── main_panel_ui.py ├── missing_file_ui.py ├── pie_menu_ui.py ├── preferences_ui.py ├── stats_panel_ui.py ├── support_me_ui.py └── utils │ └── ui_layouts.py └── updater ├── addon_updater.py └── addon_updater_ops.py /README.md: -------------------------------------------------------------------------------- 1 | [![atomic_combo_side_mono_dark1-min](https://remington.pro/wp-content/uploads/2019/08/atomic_combo_side_dark1_512px.png)](https://remington.pro/software/blender/atomic) 2 | 3 | [**Learn More About Atomic Data Manager on its Official Product Page**](https://remington.pro/software/blender/atomic) 4 | 5 | 6 | ## ENJOY A CLEANER PROJECT 7 | Atomic Data Manager offers Blender artists an intelligent data management solution. This feature-packed add-on provides artists with every tool they need to keep unwanted and unused data out of their Blender files. Even better, Atomic's real-time data analysis and automated data removal tools allow its users to clean their projects in a flash. 8 | 9 | ![spring_window_shaded-min](https://remington.pro/wp-content/uploads/2019/07/spring_window_shaded-min.jpg) 10 | 11 | 12 | ## UNMATCHED FEATURE SET 13 | 14 | | Rapid Cleaning | Missing File Detection | 15 | |--|--| 16 | | With Atomic, you can clean your project files in a snap. Simply select a category and click the clean button. | Stop those pink textures! Atomic helps you restore missing project files before you even realize they're gone. | 17 | 18 | | Inspection Tools | Data Security | 19 | |--|--| 20 | | Find out where and how data-blocks are being used, so you can make manual adjustments on the fly. | Know what you're removing before its gone. Atomic offers reliable data security features to keep your projects safe. | 21 | 22 | | Rich Statistics | Compact Design | 23 | |--|--| 24 | | Get a detailed breakdown of the data being used in your Blender projects. Surprisingly interesting and useful! | Atomic's sleek user interface packs numerous powerful features into a convenient and easily accessible space.| 25 | 26 | #### Additional Features: 27 | Pie Menu Controls, Advanced Fake User Options, Mass Delete Categories, Undo Accidental Deletions, Save Data-Blocks, Delete Data-Blocks, Replace Data-Blocks, Rename Data-Blocks, Reload Missing Files, Remove Missing Files, Replace Missing Files, and Search for Missing Files. 28 | 29 | 30 | ## TAKE A VIDEO TOUR 31 | [![video-thumb](https://remington.pro/wp-content/uploads/2019/08/atomic_addon_tour_play_button-min.jpg)](https://remington.pro/software/blender/atomic/#tour) 32 | 33 | | Keep Your Projects Clean | Reduce File Sizes | Optimize for Render Farms | 34 | |--|--|--| 35 | | Everyone appreciates a clean project. Use Atomic's intelligent toolset to keep your projects looking spiffy. | Atomic reduces file sizes by removing unused data from your projects. That way, there's more space for other stuff! | Render farms prioritize smaller projects. Atomic can optimize your files so they render sooner! | 36 | 37 | 38 | ## GET ATOMIC 39 | 40 | **Download:** [https://remington.pro/software/blender/atomic/](https://remington.pro/software/blender/atomic) 41 | 42 | **Older Versions:** [https://github.com/grantwilk/atomic-data-manager/releases](https://github.com/grantwilk/atomic-data-manager/releases) 43 | 44 | 45 | 46 | **Like the Add-on?** [Consider Supporting Remington Creative](https://remington.pro/support/)! 47 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains Atomic's global properties and handles the 22 | registration for all packages within the add-on. 23 | 24 | """ 25 | 26 | 27 | import bpy 28 | from bpy.utils import register_class 29 | from bpy.utils import unregister_class 30 | from atomic_data_manager import ops 31 | from atomic_data_manager import ui 32 | from atomic_data_manager.ui import inspect_ui 33 | from atomic_data_manager.updater import addon_updater_ops 34 | 35 | bl_info = { 36 | "name": "Atomic Data Manager", 37 | "author": "Remington Creative", 38 | "blender": (2, 80, 0), 39 | "version": (1, 0, 3), 40 | "location": "Properties > Scene", 41 | "category": "Remington Creative", 42 | "description": "An Intelligent Data Manager for Blender.", 43 | "wiki_url": 44 | "https://remington.pro/software/blender/atomic", 45 | "tracker_url": 46 | "https://github.com/grantwilk/atomic-data-manager/issues" 47 | } 48 | 49 | 50 | # Atomic Data Manager Properties 51 | class ATOMIC_PG_main(bpy.types.PropertyGroup): 52 | # main panel toggle buttons 53 | collections: bpy.props.BoolProperty(default=False) 54 | images: bpy.props.BoolProperty(default=False) 55 | lights: bpy.props.BoolProperty(default=False) 56 | materials: bpy.props.BoolProperty(default=False) 57 | node_groups: bpy.props.BoolProperty(default=False) 58 | particles: bpy.props.BoolProperty(default=False) 59 | textures: bpy.props.BoolProperty(default=False) 60 | worlds: bpy.props.BoolProperty(default=False) 61 | 62 | # inspect data-block search fields 63 | collections_field: bpy.props.StringProperty( 64 | update=inspect_ui.update_inspection) 65 | 66 | images_field: bpy.props.StringProperty( 67 | update=inspect_ui.update_inspection) 68 | 69 | lights_field: bpy.props.StringProperty( 70 | update=inspect_ui.update_inspection) 71 | 72 | materials_field: bpy.props.StringProperty( 73 | update=inspect_ui.update_inspection) 74 | 75 | node_groups_field: bpy.props.StringProperty( 76 | update=inspect_ui.update_inspection) 77 | 78 | particles_field: bpy.props.StringProperty( 79 | update=inspect_ui.update_inspection) 80 | 81 | textures_field: bpy.props.StringProperty( 82 | update=inspect_ui.update_inspection) 83 | 84 | worlds_field: bpy.props.StringProperty( 85 | update=inspect_ui.update_inspection) 86 | 87 | # enum for the inspection mode that is currently active 88 | active_inspection: bpy.props.EnumProperty( 89 | items=[ 90 | ( 91 | 'COLLECTIONS', 92 | 'Collections', 93 | 'Collections' 94 | ), 95 | ( 96 | 'IMAGES', 97 | 'Images', 98 | 'Images' 99 | ), 100 | ( 101 | 'LIGHTS', 102 | 'Lights', 103 | 'Lights' 104 | ), 105 | ( 106 | 'MATERIALS', 107 | 'Materials', 108 | 'Materials' 109 | ), 110 | ( 111 | 'NODE_GROUPS', 112 | 'Node Groups', 113 | 'Node Groups' 114 | ), 115 | ( 116 | 'PARTICLES', 117 | 'Particles', 118 | 'Particles' 119 | ), 120 | ( 121 | 'TEXTURES', 122 | 'Textures', 123 | 'Textures' 124 | ), 125 | ( 126 | 'WORLDS', 127 | 'Worlds', 128 | 'Worlds' 129 | ) 130 | ], 131 | default='COLLECTIONS' 132 | ) 133 | 134 | # enum for the type of data being shown in the stats panel 135 | stats_mode: bpy.props.EnumProperty( 136 | items=[ 137 | ( 138 | 'OVERVIEW', # identifier 139 | 'Overview', # title 140 | 'Overview', # description 141 | 'FILE', # icon 142 | 0 # number / id 143 | ), 144 | ( 145 | 'COLLECTIONS', 146 | 'Collections', 147 | 'Collections', 148 | 'GROUP', 149 | 1 150 | ), 151 | ( 152 | 'IMAGES', 153 | 'Images', 154 | 'Images', 155 | 'IMAGE_DATA', 156 | 2 157 | ), 158 | ( 159 | 'LIGHTS', 160 | 'Lights', 161 | 'Lights', 162 | 'LIGHT', 163 | 3 164 | ), 165 | ( 166 | 'MATERIALS', 167 | 'Materials', 168 | 'Materials', 169 | 'MATERIAL', 170 | 4 171 | ), 172 | ( 173 | 'OBJECTS', 174 | 'Objects', 175 | 'Objects', 176 | 'OBJECT_DATA', 177 | 5 178 | ), 179 | ( 180 | 'NODE_GROUPS', 181 | 'Node Groups', 182 | 'Node Groups', 183 | 'NODETREE', 184 | 6 185 | ), 186 | ( 187 | 'PARTICLES', 188 | 'Particle Systems', 189 | 'Particle Systems', 190 | 'PARTICLES', 191 | 7 192 | ), 193 | ( 194 | 'TEXTURES', 195 | 'Textures', 196 | 'Textures', 197 | 'TEXTURE', 198 | 8 199 | ), 200 | ( 201 | 'WORLDS', 202 | 'Worlds', 203 | 'Worlds', 204 | 'WORLD', 205 | 9 206 | ) 207 | ], 208 | default='OVERVIEW' 209 | ) 210 | 211 | # text field for the inspect rename operator 212 | rename_field: bpy.props.StringProperty() 213 | 214 | # search field for the inspect replace operator 215 | replace_field: bpy.props.StringProperty() 216 | 217 | 218 | def register(): 219 | # add-on updater registration 220 | addon_updater_ops.register(bl_info) 221 | 222 | register_class(ATOMIC_PG_main) 223 | bpy.types.Scene.atomic = bpy.props.PointerProperty(type=ATOMIC_PG_main) 224 | 225 | # atomic package registration 226 | ui.register() 227 | ops.register() 228 | 229 | 230 | def unregister(): 231 | 232 | # add-on updated unregistration 233 | addon_updater_ops.unregister() 234 | 235 | # atomic package unregistration 236 | ui.unregister() 237 | ops.unregister() 238 | 239 | unregister_class(ATOMIC_PG_main) 240 | del bpy.types.Scene.atomic 241 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains global copies of Atomic's preferences so that they 22 | can be easily access throughout the add-on. 23 | 24 | NOTE: 25 | Changing the values of these variables will NOT change the values in the 26 | Atomic's preferences. If you want to change a setting, change it in 27 | Blender, not in here. 28 | 29 | """ 30 | 31 | # visible atomic preferences 32 | enable_missing_file_warning = True 33 | enable_support_me_popup = True 34 | include_fake_users = False 35 | enable_pie_menu_ui = True 36 | 37 | # hidden atomic preferences 38 | pie_menu_type = "D" 39 | pie_menu_alt = False 40 | pie_menu_any = False 41 | pie_menu_ctrl = False 42 | pie_menu_oskey = False 43 | pie_menu_shift = False 44 | last_popup_day = 0 -------------------------------------------------------------------------------- /ops/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file handles the registration of the atomic_data_manager.ops package 22 | 23 | """ 24 | 25 | from atomic_data_manager.ops import main_ops 26 | from atomic_data_manager.ops import inspect_ops 27 | from atomic_data_manager.ops import direct_use_ops 28 | from atomic_data_manager.ops import missing_file_ops 29 | from atomic_data_manager.ops import support_me_ops 30 | 31 | 32 | def register(): 33 | main_ops.register() 34 | inspect_ops.register() 35 | direct_use_ops.register() 36 | missing_file_ops.register() 37 | support_me_ops.register() 38 | 39 | 40 | def unregister(): 41 | main_ops.unregister() 42 | inspect_ops.unregister() 43 | direct_use_ops.unregister() 44 | missing_file_ops.unregister() 45 | support_me_ops.unregister() -------------------------------------------------------------------------------- /ops/direct_use_ops.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the direct use operators, intended to be used with 22 | Atomic's pie menu interface. However, they can be implemented anywhere 23 | if they need to be. 24 | 25 | These operators basically wrap the functions from ops.utils.nuke.py and 26 | ops.utils.clean.py into operators so they can be easily called by other 27 | intefaces in Blender. 28 | 29 | """ 30 | 31 | import bpy 32 | from bpy.utils import register_class 33 | from bpy.utils import unregister_class 34 | from atomic_data_manager import config 35 | from atomic_data_manager.stats import unused 36 | from atomic_data_manager.ops.utils import nuke 37 | from atomic_data_manager.ops.utils import clean 38 | from atomic_data_manager.ui.utils import ui_layouts 39 | 40 | 41 | class ATOMIC_OT_invoke_pie_menu_ui(bpy.types.Operator): 42 | """Invokes Atomic's pie menu UI if the \"Enable Pie Menu UI\" 43 | preference is enabled in Atomic's preferences panel""" 44 | bl_idname = "atomic.invoke_pie_menu_ui" 45 | bl_label = "Invoke Pie Menu UI" 46 | 47 | def execute(self, context): 48 | if config.enable_pie_menu_ui: 49 | bpy.ops.wm.call_menu_pie(name="ATOMIC_MT_main_pie") 50 | return {'FINISHED'} 51 | 52 | 53 | # Atomic Data Manager Nuke All Operator 54 | class ATOMIC_OT_nuke_all(bpy.types.Operator): 55 | """Remove all data-blocks from the selected categories""" 56 | bl_idname = "atomic.nuke_all" 57 | bl_label = "CAUTION!" 58 | 59 | def draw(self, context): 60 | layout = self.layout 61 | 62 | col = layout.column() 63 | col.label(text="Remove the following data-blocks?") 64 | 65 | collections = sorted(bpy.data.collections.keys()) 66 | ui_layouts.box_list( 67 | layout=layout, 68 | title="Collections", 69 | items=collections, 70 | icon="OUTLINER_OB_GROUP_INSTANCE" 71 | ) 72 | 73 | images = sorted(bpy.data.images.keys()) 74 | ui_layouts.box_list( 75 | layout=layout, 76 | title="Images", 77 | items=images, 78 | icon="IMAGE_DATA" 79 | ) 80 | 81 | lights = sorted(bpy.data.lights.keys()) 82 | ui_layouts.box_list( 83 | layout=layout, 84 | title="Lights", 85 | items=lights, 86 | icon="OUTLINER_OB_LIGHT" 87 | ) 88 | 89 | materials = sorted(bpy.data.materials.keys()) 90 | ui_layouts.box_list( 91 | layout=layout, 92 | title="Materials", 93 | items=materials, 94 | icon="MATERIAL" 95 | ) 96 | 97 | node_groups = sorted(bpy.data.node_groups.keys()) 98 | ui_layouts.box_list( 99 | layout=layout, 100 | title="Node Groups", 101 | items=node_groups, 102 | icon="NODETREE" 103 | ) 104 | 105 | particles = sorted(bpy.data.particles.keys()) 106 | ui_layouts.box_list( 107 | layout=layout, 108 | title="Particle Systems", 109 | items=particles, 110 | icon="PARTICLES" 111 | ) 112 | 113 | textures = sorted(bpy.data.textures.keys()) 114 | ui_layouts.box_list( 115 | layout=layout, 116 | title="Textures", 117 | items=textures, 118 | icon="TEXTURE" 119 | ) 120 | 121 | worlds = sorted(bpy.data.worlds.keys()) 122 | ui_layouts.box_list( 123 | layout=layout, 124 | title="Worlds", 125 | items=worlds, 126 | icon="WORLD" 127 | ) 128 | 129 | row = layout.row() # extra spacing 130 | 131 | def execute(self, context): 132 | 133 | nuke.collections() 134 | nuke.images() 135 | nuke.lights() 136 | nuke.materials() 137 | nuke.node_groups() 138 | nuke.particles() 139 | nuke.textures() 140 | nuke.worlds() 141 | 142 | return {'FINISHED'} 143 | 144 | def invoke(self, context, event): 145 | wm = context.window_manager 146 | return wm.invoke_props_dialog(self) 147 | 148 | 149 | # Atomic Data Manager Clean All Operator 150 | class ATOMIC_OT_clean_all(bpy.types.Operator): 151 | """Remove all unused data-blocks from the selected categories""" 152 | bl_idname = "atomic.clean_all" 153 | bl_label = "Clean All" 154 | 155 | unused_collections = [] 156 | unused_images = [] 157 | unused_lights = [] 158 | unused_materials = [] 159 | unused_node_groups = [] 160 | unused_particles = [] 161 | unused_textures = [] 162 | unused_worlds = [] 163 | 164 | def draw(self, context): 165 | layout = self.layout 166 | 167 | col = layout.column() 168 | col.label(text="Remove the following data-blocks?") 169 | 170 | collections = sorted(unused.collections_deep()) 171 | ui_layouts.box_list( 172 | layout=layout, 173 | title="Collections", 174 | items=collections, 175 | icon="OUTLINER_OB_GROUP_INSTANCE" 176 | ) 177 | 178 | images = sorted(unused.images_deep()) 179 | ui_layouts.box_list( 180 | layout=layout, 181 | title="Images", 182 | items=images, 183 | icon="IMAGE_DATA" 184 | ) 185 | 186 | lights = sorted(unused.lights_deep()) 187 | ui_layouts.box_list( 188 | layout=layout, 189 | title="Lights", 190 | items=lights, 191 | icon="OUTLINER_OB_LIGHT" 192 | ) 193 | 194 | materials = sorted(unused.materials_deep()) 195 | ui_layouts.box_list( 196 | layout=layout, 197 | title="Materials", 198 | items=materials, 199 | icon="MATERIAL" 200 | ) 201 | 202 | node_groups = sorted(unused.node_groups_deep()) 203 | ui_layouts.box_list( 204 | layout=layout, 205 | title="Node Groups", 206 | items=node_groups, 207 | icon="NODETREE" 208 | ) 209 | 210 | particles = sorted(unused.particles_deep()) 211 | ui_layouts.box_list( 212 | layout=layout, 213 | title="Particle Systems", 214 | items=particles, 215 | icon="PARTICLES" 216 | ) 217 | 218 | textures = sorted(unused.textures_deep()) 219 | ui_layouts.box_list( 220 | layout=layout, 221 | title="Textures", 222 | items=textures, 223 | icon="TEXTURE" 224 | ) 225 | 226 | worlds = sorted(unused.worlds()) 227 | ui_layouts.box_list( 228 | layout=layout, 229 | title="Worlds", 230 | items=worlds, 231 | icon="WORLD" 232 | ) 233 | 234 | row = layout.row() # extra spacing 235 | 236 | def execute(self, context): 237 | 238 | clean.collections() 239 | clean.images() 240 | clean.lights() 241 | clean.materials() 242 | clean.node_groups() 243 | clean.particles() 244 | clean.textures() 245 | clean.worlds() 246 | 247 | return {'FINISHED'} 248 | 249 | def invoke(self, context, event): 250 | wm = context.window_manager 251 | 252 | self.unused_collections = unused.collections_deep() 253 | self.unused_images = unused.images_deep() 254 | self.unused_lights = unused.lights_deep() 255 | self.unused_materials = unused.materials_deep() 256 | self.unused_node_groups = unused.node_groups_deep() 257 | self.unused_particles = unused.particles_deep() 258 | self.unused_textures = unused.textures_deep() 259 | self.unused_worlds = unused.worlds() 260 | 261 | return wm.invoke_props_dialog(self) 262 | 263 | 264 | # Atomic Data Manager Nuke Collections Operator 265 | class ATOMIC_OT_nuke_collections(bpy.types.Operator): 266 | """Remove all collections from this project""" 267 | bl_idname = "atomic.nuke_collections" 268 | bl_label = "Nuke Collections" 269 | 270 | def draw(self, context): 271 | layout = self.layout 272 | 273 | row = layout.row() 274 | row.label(text="Remove the following data-blocks?") 275 | 276 | collections = bpy.data.collections.keys() 277 | ui_layouts.box_list( 278 | layout=layout, 279 | items=collections, 280 | icon="OUTLINER_OB_GROUP_INSTANCE" 281 | ) 282 | 283 | row = layout.row() # extra space 284 | 285 | def execute(self, context): 286 | nuke.collections() 287 | return {'FINISHED'} 288 | 289 | def invoke(self, context, event): 290 | wm = context.window_manager 291 | return wm.invoke_props_dialog(self) 292 | 293 | 294 | # Atomic Data Manager Nuke Images Operator 295 | class ATOMIC_OT_nuke_images(bpy.types.Operator): 296 | """Remove all images from this project""" 297 | bl_idname = "atomic.nuke_images" 298 | bl_label = "Nuke Images" 299 | 300 | def draw(self, context): 301 | layout = self.layout 302 | 303 | row = layout.row() 304 | row.label(text="Remove the following data-blocks?") 305 | 306 | images = bpy.data.images.keys() 307 | ui_layouts.box_list( 308 | layout=layout, 309 | items=images, 310 | icon="IMAGE_DATA" 311 | ) 312 | 313 | row = layout.row() # extra space 314 | 315 | def execute(self, context): 316 | nuke.images() 317 | return {'FINISHED'} 318 | 319 | def invoke(self, context, event): 320 | wm = context.window_manager 321 | return wm.invoke_props_dialog(self) 322 | 323 | 324 | # Atomic Data Manager Nuke Lights Operator 325 | class ATOMIC_OT_nuke_lights(bpy.types.Operator): 326 | """Remove all lights from this project""" 327 | bl_idname = "atomic.nuke_lights" 328 | bl_label = "Nuke Lights" 329 | 330 | def draw(self, context): 331 | layout = self.layout 332 | 333 | row = layout.row() 334 | row.label(text="Remove the following data-blocks?") 335 | 336 | lights = bpy.data.lights.keys() 337 | ui_layouts.box_list( 338 | layout=layout, 339 | items=lights, 340 | icon="OUTLINER_OB_LIGHT" 341 | ) 342 | 343 | row = layout.row() # extra space 344 | 345 | def execute(self, context): 346 | nuke.lights() 347 | return {'FINISHED'} 348 | 349 | def invoke(self, context, event): 350 | wm = context.window_manager 351 | return wm.invoke_props_dialog(self) 352 | 353 | 354 | # Atomic Data Manager Nuke Materials Operator 355 | class ATOMIC_OT_nuke_materials(bpy.types.Operator): 356 | """Remove all materials from this project""" 357 | bl_idname = "atomic.nuke_materials" 358 | bl_label = "Nuke Materials" 359 | 360 | def draw(self, context): 361 | layout = self.layout 362 | 363 | row = layout.row() 364 | row.label(text="Remove the following data-blocks?") 365 | 366 | materials = bpy.data.materials.keys() 367 | ui_layouts.box_list( 368 | layout=layout, 369 | items=materials, 370 | icon="MATERIAL" 371 | ) 372 | 373 | row = layout.row() # extra space 374 | 375 | def execute(self, context): 376 | nuke.materials() 377 | return {'FINISHED'} 378 | 379 | def invoke(self, context, event): 380 | wm = context.window_manager 381 | return wm.invoke_props_dialog(self) 382 | 383 | 384 | # Atomic Data Manager Nuke Node Groups Operator 385 | class ATOMIC_OT_nuke_node_groups(bpy.types.Operator): 386 | """Remove all node groups from this project""" 387 | bl_idname = "atomic.nuke_node_groups" 388 | bl_label = "Nuke Node Groups" 389 | 390 | def draw(self, context): 391 | layout = self.layout 392 | 393 | row = layout.row() 394 | row.label(text="Remove the following data-blocks?") 395 | 396 | node_groups = bpy.data.node_groups.keys() 397 | ui_layouts.box_list( 398 | layout=layout, 399 | items=node_groups, 400 | icon="NODETREE" 401 | ) 402 | 403 | row = layout.row() # extra space 404 | 405 | def execute(self, context): 406 | nuke.node_groups() 407 | return {'FINISHED'} 408 | 409 | def invoke(self, context, event): 410 | wm = context.window_manager 411 | return wm.invoke_props_dialog(self) 412 | 413 | 414 | # Atomic Data Manager Nuke Particles Operator 415 | class ATOMIC_OT_nuke_particles(bpy.types.Operator): 416 | """Remove all particle systems from this project""" 417 | bl_idname = "atomic.nuke_particles" 418 | bl_label = "Nuke Particles" 419 | 420 | def draw(self, context): 421 | layout = self.layout 422 | 423 | row = layout.row() 424 | row.label(text="Remove the following data-blocks?") 425 | 426 | particles = bpy.data.particles.keys() 427 | ui_layouts.box_list( 428 | layout=layout, 429 | items=particles, 430 | icon="PARTICLES" 431 | ) 432 | 433 | row = layout.row() # extra space 434 | 435 | def execute(self, context): 436 | nuke.particles() 437 | return {'FINISHED'} 438 | 439 | def invoke(self, context, event): 440 | wm = context.window_manager 441 | return wm.invoke_props_dialog(self) 442 | 443 | 444 | # Atomic Data Manager Nuke Textures Operator 445 | class ATOMIC_OT_nuke_textures(bpy.types.Operator): 446 | """Remove all textures from this project""" 447 | bl_idname = "atomic.nuke_textures" 448 | bl_label = "Nuke Textures" 449 | 450 | def draw(self, context): 451 | layout = self.layout 452 | 453 | row = layout.row() 454 | row.label(text="Remove the following data-blocks?") 455 | 456 | textures = bpy.data.textures.keys() 457 | ui_layouts.box_list( 458 | layout=layout, 459 | items=textures, 460 | icon="TEXTURE" 461 | ) 462 | 463 | row = layout.row() # extra space 464 | 465 | def execute(self, context): 466 | nuke.textures() 467 | return {'FINISHED'} 468 | 469 | def invoke(self, context, event): 470 | wm = context.window_manager 471 | return wm.invoke_props_dialog(self) 472 | 473 | 474 | # Atomic Data Manager Nuke Worlds Operator 475 | class ATOMIC_OT_nuke_worlds(bpy.types.Operator): 476 | """Remove all worlds from this project""" 477 | bl_idname = "atomic.nuke_worlds" 478 | bl_label = "Nuke Worlds" 479 | 480 | def draw(self, context): 481 | layout = self.layout 482 | 483 | row = layout.row() 484 | row.label(text="Remove the following data-blocks?") 485 | 486 | worlds = bpy.data.worlds.keys() 487 | ui_layouts.box_list( 488 | layout=layout, 489 | items=worlds, 490 | icon="WORLD" 491 | ) 492 | 493 | row = layout.row() # extra space 494 | 495 | def execute(self, context): 496 | nuke.worlds() 497 | return {'FINISHED'} 498 | 499 | def invoke(self, context, event): 500 | wm = context.window_manager 501 | return wm.invoke_props_dialog(self) 502 | 503 | 504 | # Atomic Data Manager Clean Collections Operator 505 | class ATOMIC_OT_clean_collections(bpy.types.Operator): 506 | """Remove all unused collections from this project""" 507 | bl_idname = "atomic.clean_collections" 508 | bl_label = "Clean Collections" 509 | 510 | unused_collections = [] 511 | 512 | def draw(self, context): 513 | layout = self.layout 514 | 515 | row = layout.row() 516 | row.label(text="Remove the following data-blocks?") 517 | 518 | ui_layouts.box_list( 519 | layout=layout, 520 | items=self.unused_collections, 521 | icon="OUTLINER_OB_GROUP_INSTANCE" 522 | ) 523 | 524 | row = layout.row() # extra space 525 | 526 | def execute(self, context): 527 | clean.collections() 528 | return {'FINISHED'} 529 | 530 | def invoke(self, context, event): 531 | wm = context.window_manager 532 | self.unused_collections = unused.collections_deep() 533 | return wm.invoke_props_dialog(self) 534 | 535 | 536 | # Atomic Data Manager Clean Images Operator 537 | class ATOMIC_OT_clean_images(bpy.types.Operator): 538 | """Remove all unused images from this project""" 539 | bl_idname = "atomic.clean_images" 540 | bl_label = "Clean Images" 541 | 542 | unused_images = [] 543 | 544 | def draw(self, context): 545 | layout = self.layout 546 | 547 | row = layout.row() 548 | row.label(text="Remove the following data-blocks?") 549 | 550 | ui_layouts.box_list( 551 | layout=layout, 552 | items=self.unused_images, 553 | icon="IMAGE_DATA" 554 | ) 555 | 556 | row = layout.row() # extra space 557 | 558 | def execute(self, context): 559 | clean.images() 560 | return {'FINISHED'} 561 | 562 | def invoke(self, context, event): 563 | wm = context.window_manager 564 | self.unused_images =unused.images_deep() 565 | return wm.invoke_props_dialog(self) 566 | 567 | 568 | # Atomic Data Manager Clean Lights Operator 569 | class ATOMIC_OT_clean_lights(bpy.types.Operator): 570 | """Remove all unused lights from this project""" 571 | bl_idname = "atomic.clean_lights" 572 | bl_label = "Clean Lights" 573 | 574 | unused_lights = [] 575 | 576 | def draw(self, context): 577 | layout = self.layout 578 | 579 | row = layout.row() 580 | row.label(text="Remove the following data-blocks?") 581 | 582 | ui_layouts.box_list( 583 | layout=layout, 584 | items=self.unused_lights, 585 | icon="OUTLINER_OB_LIGHT" 586 | ) 587 | 588 | row = layout.row() # extra space 589 | 590 | def execute(self, context): 591 | clean.lights() 592 | return {'FINISHED'} 593 | 594 | def invoke(self, context, event): 595 | wm = context.window_manager 596 | self.unused_lights = unused.lights_deep() 597 | return wm.invoke_props_dialog(self) 598 | 599 | 600 | # Atomic Data Manager Clean Materials Operator 601 | class ATOMIC_OT_clean_materials(bpy.types.Operator): 602 | """Remove all unused materials from this project""" 603 | bl_idname = "atomic.clean_materials" 604 | bl_label = "Clean Materials" 605 | 606 | unused_materials = [] 607 | 608 | def draw(self, context): 609 | layout = self.layout 610 | 611 | row = layout.row() 612 | row.label(text="Remove the following data-blocks?") 613 | 614 | ui_layouts.box_list( 615 | layout=layout, 616 | items=self.unused_materials, 617 | icon="MATERIAL" 618 | ) 619 | 620 | row = layout.row() # extra space 621 | 622 | def execute(self, context): 623 | clean.materials() 624 | return {'FINISHED'} 625 | 626 | def invoke(self, context, event): 627 | wm = context.window_manager 628 | self.unused_materials = unused.materials_deep() 629 | return wm.invoke_props_dialog(self) 630 | 631 | 632 | # Atomic Data Manager Clean Node Groups Operator 633 | class ATOMIC_OT_clean_node_groups(bpy.types.Operator): 634 | """Remove all unused node groups from this project""" 635 | bl_idname = "atomic.clean_node_groups" 636 | bl_label = "Clean Node Groups" 637 | 638 | unused_node_groups = [] 639 | 640 | def draw(self, context): 641 | layout = self.layout 642 | 643 | row = layout.row() 644 | row.label(text="Remove the following data-blocks?") 645 | 646 | ui_layouts.box_list( 647 | layout=layout, 648 | items=self.unused_node_groups, 649 | icon="NODETREE" 650 | ) 651 | 652 | row = layout.row() # extra space 653 | 654 | def execute(self, context): 655 | clean.node_groups() 656 | return {'FINISHED'} 657 | 658 | def invoke(self, context, event): 659 | wm = context.window_manager 660 | self.unused_node_groups = unused.node_groups_deep() 661 | return wm.invoke_props_dialog(self) 662 | 663 | 664 | # Atomic Data Manager Clean Particles Operator 665 | class ATOMIC_OT_clean_particles(bpy.types.Operator): 666 | """Remove all unused particle systems from this project""" 667 | bl_idname = "atomic.clean_particles" 668 | bl_label = "Clean Particles" 669 | 670 | unused_particles = [] 671 | 672 | def draw(self, context): 673 | layout = self.layout 674 | 675 | row = layout.row() 676 | row.label(text="Remove the following data-blocks?") 677 | 678 | ui_layouts.box_list( 679 | layout=layout, 680 | items=self.unused_particles, 681 | icon="PARTICLES" 682 | ) 683 | 684 | row = layout.row() # extra space 685 | 686 | def execute(self, context): 687 | clean.particles() 688 | return {'FINISHED'} 689 | 690 | def invoke(self, context, event): 691 | wm = context.window_manager 692 | self.unused_particles = unused.particles_deep() 693 | return wm.invoke_props_dialog(self) 694 | 695 | 696 | # Atomic Data Manager Clean Textures Operator 697 | class ATOMIC_OT_clean_textures(bpy.types.Operator): 698 | """Remove all unused textures from this project""" 699 | bl_idname = "atomic.clean_textures" 700 | bl_label = "Clean Textures" 701 | 702 | unused_textures = [] 703 | 704 | def draw(self, context): 705 | layout = self.layout 706 | 707 | row = layout.row() 708 | row.label(text="Remove the following data-blocks?") 709 | 710 | ui_layouts.box_list( 711 | layout=layout, 712 | items=self.unused_textures, 713 | icon="TEXTURE" 714 | ) 715 | 716 | row = layout.row() # extra space 717 | 718 | def execute(self, context): 719 | clean.textures() 720 | return {'FINISHED'} 721 | 722 | def invoke(self, context, event): 723 | wm = context.window_manager 724 | self.unused_textures = unused.textures_deep() 725 | return wm.invoke_props_dialog(self) 726 | 727 | 728 | # Atomic Data Manager Clean Worlds Operator 729 | class ATOMIC_OT_clean_worlds(bpy.types.Operator): 730 | """Remove all unused worlds from this project""" 731 | bl_idname = "atomic.clean_worlds" 732 | bl_label = "Clean Worlds" 733 | 734 | unused_worlds = [] 735 | 736 | def draw(self, context): 737 | layout = self.layout 738 | 739 | row = layout.row() 740 | row.label(text="Remove the following data-blocks?") 741 | 742 | ui_layouts.box_list( 743 | layout=layout, 744 | items=self.unused_worlds, 745 | icon="WORLD" 746 | ) 747 | 748 | row = layout.row() # extra space 749 | 750 | def execute(self, context): 751 | clean.worlds() 752 | return {'FINISHED'} 753 | 754 | def invoke(self, context, event): 755 | wm = context.window_manager 756 | self.unused_worlds = unused.worlds() 757 | return wm.invoke_props_dialog(self) 758 | 759 | 760 | reg_list = [ 761 | ATOMIC_OT_invoke_pie_menu_ui, 762 | 763 | ATOMIC_OT_nuke_all, 764 | ATOMIC_OT_clean_all, 765 | 766 | ATOMIC_OT_nuke_collections, 767 | ATOMIC_OT_nuke_images, 768 | ATOMIC_OT_nuke_lights, 769 | ATOMIC_OT_nuke_materials, 770 | ATOMIC_OT_nuke_node_groups, 771 | ATOMIC_OT_nuke_particles, 772 | ATOMIC_OT_nuke_textures, 773 | ATOMIC_OT_nuke_worlds, 774 | 775 | ATOMIC_OT_clean_collections, 776 | ATOMIC_OT_clean_images, 777 | ATOMIC_OT_clean_lights, 778 | ATOMIC_OT_clean_materials, 779 | ATOMIC_OT_clean_node_groups, 780 | ATOMIC_OT_clean_particles, 781 | ATOMIC_OT_clean_textures, 782 | ATOMIC_OT_clean_worlds 783 | ] 784 | 785 | 786 | def register(): 787 | for item in reg_list: 788 | register_class(item) 789 | 790 | 791 | def unregister(): 792 | for item in reg_list: 793 | unregister_class(item) 794 | -------------------------------------------------------------------------------- /ops/inspect_ops.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the operators used in the inspection UI's header. 22 | This includes the rename, replace, toggle fake user, delete, and duplicate 23 | operators. 24 | 25 | """ 26 | 27 | import bpy 28 | from bpy.utils import register_class 29 | from bpy.utils import unregister_class 30 | from atomic_data_manager.ops.utils import delete 31 | from atomic_data_manager.ops.utils import duplicate 32 | 33 | 34 | # Atomic Data Manager Inspection Rename Operator 35 | class ATOMIC_OT_inspection_rename(bpy.types.Operator): 36 | """Give this data-block a new name""" 37 | bl_idname = "atomic.rename" 38 | bl_label = "Rename Data-Block" 39 | 40 | def draw(self, context): 41 | atom = bpy.context.scene.atomic 42 | 43 | layout = self.layout 44 | row = layout.row() 45 | row.prop(atom, "rename_field", text="", icon="GREASEPENCIL") 46 | 47 | def execute(self, context): 48 | atom = bpy.context.scene.atomic 49 | inspection = atom.active_inspection 50 | 51 | name = atom.rename_field 52 | 53 | if inspection == 'COLLECTIONS': 54 | bpy.data.collections[atom.collections_field].name = name 55 | atom.collections_field = name 56 | 57 | if inspection == 'IMAGES': 58 | bpy.data.images[atom.images_field].name = name 59 | atom.images_field = name 60 | 61 | if inspection == 'LIGHTS': 62 | bpy.data.lights[atom.lights_field].name = name 63 | atom.lights_field = name 64 | 65 | if inspection == 'MATERIALS': 66 | bpy.data.materials[atom.materials_field].name = name 67 | atom.materials_field = name 68 | 69 | if inspection == 'NODE_GROUPS': 70 | bpy.data.node_groups[atom.node_groups_field].name = name 71 | atom.node_groups_field = name 72 | 73 | if inspection == 'PARTICLES': 74 | bpy.data.particles[atom.particles_field].name = name 75 | atom.particles_field = name 76 | 77 | if inspection == 'TEXTURES': 78 | bpy.data.textures[atom.textures_field].name = name 79 | atom.textures_field = name 80 | 81 | if inspection == 'WORLDS': 82 | bpy.data.worlds[atom.worlds_field].name = name 83 | atom.worlds_field = name 84 | 85 | atom.rename_field = "" 86 | return {'FINISHED'} 87 | 88 | def invoke(self, context, event): 89 | wm = context.window_manager 90 | return wm.invoke_props_dialog(self, width=200) 91 | 92 | 93 | # Atomic Data Manager Inspection Replaces Operator 94 | class ATOMIC_OT_inspection_replace(bpy.types.Operator): 95 | """Replace all instances of this data-block with another data-block""" 96 | bl_idname = "atomic.replace" 97 | bl_label = "Replace Data-Block" 98 | 99 | def draw(self, context): 100 | atom = bpy.context.scene.atomic 101 | inspection = atom.active_inspection 102 | 103 | layout = self.layout 104 | row = layout.row() 105 | 106 | if inspection == 'IMAGES': 107 | row.prop_search( 108 | atom, 109 | "replace_field", 110 | bpy.data, 111 | "images", 112 | text="" 113 | ) 114 | 115 | if inspection == 'LIGHTS': 116 | row.prop_search( 117 | atom, 118 | "replace_field", 119 | bpy.data, 120 | "lights", 121 | text="" 122 | ) 123 | 124 | if inspection == 'MATERIALS': 125 | row.prop_search( 126 | atom, 127 | "replace_field", 128 | bpy.data, 129 | "materials", 130 | text="" 131 | ) 132 | 133 | if inspection == 'NODE_GROUPS': 134 | row.prop_search( 135 | atom, 136 | "replace_field", 137 | bpy.data, 138 | "node_groups", 139 | text="" 140 | ) 141 | 142 | if inspection == 'PARTICLES': 143 | row.prop_search( 144 | atom, 145 | "replace_field", 146 | bpy.data, 147 | "particles", 148 | text="" 149 | ) 150 | 151 | if inspection == 'TEXTURES': 152 | row.prop_search( 153 | atom, 154 | "replace_field", 155 | bpy.data, 156 | "textures", 157 | text="" 158 | ) 159 | 160 | if inspection == 'WORLDS': 161 | row.prop_search( 162 | atom, 163 | "replace_field", 164 | bpy.data, 165 | "worlds", 166 | text="" 167 | ) 168 | 169 | def execute(self, context): 170 | atom = bpy.context.scene.atomic 171 | inspection = atom.active_inspection 172 | 173 | if inspection == 'IMAGES' and \ 174 | atom.replace_field in bpy.data.images.keys(): 175 | bpy.data.images[atom.images_field].user_remap( 176 | bpy.data.images[atom.replace_field]) 177 | atom.images_field = atom.replace_field 178 | 179 | if inspection == 'LIGHTS' and \ 180 | atom.replace_field in bpy.data.lights.keys(): 181 | bpy.data.lights[atom.lights_field].user_remap( 182 | bpy.data.lights[atom.replace_field]) 183 | atom.lights_field = atom.replace_field 184 | 185 | if inspection == 'MATERIALS' and \ 186 | atom.replace_field in bpy.data.materials.keys(): 187 | bpy.data.materials[atom.materials_field].user_remap( 188 | bpy.data.materials[atom.replace_field]) 189 | atom.materials_field = atom.replace_field 190 | 191 | if inspection == 'NODE_GROUPS' and \ 192 | atom.replace_field in bpy.data.node_groups.keys(): 193 | bpy.data.node_groups[atom.node_groups_field].user_remap( 194 | bpy.data.node_groups[atom.replace_field]) 195 | atom.node_groups_field = atom.replace_field 196 | 197 | if inspection == 'PARTICLES' and \ 198 | atom.replace_field in bpy.data.particles.keys(): 199 | bpy.data.particles[atom.particles_field].user_remap( 200 | bpy.data.particles[atom.replace_field]) 201 | atom.particles_field = atom.replace_field 202 | 203 | if inspection == 'TEXTURES' and \ 204 | atom.replace_field in bpy.data.textures.keys(): 205 | bpy.data.textures[atom.textures_field].user_remap( 206 | bpy.data.textures[atom.replace_field]) 207 | atom.textures_field = atom.replace_field 208 | 209 | if inspection == 'WORLDS' and \ 210 | atom.replace_field in bpy.data.worlds.keys(): 211 | bpy.data.worlds[atom.worlds_field].user_remap( 212 | bpy.data.worlds[atom.replace_field]) 213 | atom.worlds_field = atom.replace_field 214 | 215 | atom.replace_field = "" 216 | return {'FINISHED'} 217 | 218 | def invoke(self, context, event): 219 | wm = context.window_manager 220 | return wm.invoke_props_dialog(self, width=200) 221 | 222 | 223 | # Atomic Data Manager Inspection Toggle Fake User Operator 224 | class ATOMIC_OT_inspection_toggle_fake_user(bpy.types.Operator): 225 | """Save this data-block even if it has no users""" 226 | bl_idname = "atomic.toggle_fake_user" 227 | bl_label = "Toggle Fake User" 228 | 229 | def execute(self, context): 230 | atom = bpy.context.scene.atomic 231 | inspection = atom.active_inspection 232 | 233 | if inspection == 'IMAGES': 234 | image = bpy.data.images[atom.images_field] 235 | bpy.data.images[atom.images_field].use_fake_user = \ 236 | not image.use_fake_user 237 | 238 | if inspection == 'LIGHTS': 239 | light = bpy.data.lights[atom.lights_field] 240 | bpy.data.lights[atom.lights_field].use_fake_user = \ 241 | not light.use_fake_user 242 | 243 | if inspection == 'MATERIALS': 244 | material = bpy.data.materials[atom.materials_field] 245 | bpy.data.materials[atom.materials_field].use_fake_user = \ 246 | not material.use_fake_user 247 | 248 | if inspection == 'NODE_GROUPS': 249 | node_group = bpy.data.node_groups[atom.node_groups_field] 250 | bpy.data.node_groups[atom.node_groups_field].use_fake_user = \ 251 | not node_group.use_fake_user 252 | 253 | if inspection == 'PARTICLES': 254 | particle = bpy.data.particles[atom.particles_field] 255 | bpy.data.particles[atom.particles_field].use_fake_user = \ 256 | not particle.use_fake_user 257 | 258 | if inspection == 'TEXTURES': 259 | texture = bpy.data.textures[atom.textures_field] 260 | bpy.data.textures[atom.textures_field].use_fake_user = \ 261 | not texture.use_fake_user 262 | 263 | if inspection == 'WORLDS': 264 | world = bpy.data.worlds[atom.worlds_field] 265 | bpy.data.worlds[atom.worlds_field].use_fake_user = \ 266 | not world.use_fake_user 267 | 268 | return {'FINISHED'} 269 | 270 | 271 | # Atomic Data Manager Inspection Duplicate Operator 272 | class ATOMIC_OT_inspection_duplicate(bpy.types.Operator): 273 | """Make an exact copy of this data-block""" 274 | bl_idname = "atomic.inspection_duplicate" 275 | bl_label = "Duplicate Data-Block" 276 | 277 | def execute(self, context): 278 | atom = bpy.context.scene.atomic 279 | inspection = atom.active_inspection 280 | 281 | if inspection == 'COLLECTIONS': 282 | key = atom.collections_field 283 | collections = bpy.data.collections 284 | 285 | if key in collections.keys(): 286 | copy_key = duplicate.collection(key) 287 | atom.collections_field = copy_key 288 | 289 | elif inspection == 'IMAGES': 290 | key = atom.images_field 291 | images = bpy.data.images 292 | 293 | if key in images.keys(): 294 | copy_key = duplicate.image(key) 295 | atom.images_field = copy_key 296 | 297 | elif inspection == 'LIGHTS': 298 | key = atom.lights_field 299 | lights = bpy.data.lights 300 | 301 | if key in lights.keys(): 302 | copy_key = duplicate.light(key) 303 | atom.lights_field = copy_key 304 | 305 | elif inspection == 'MATERIALS': 306 | key = atom.materials_field 307 | materials = bpy.data.materials 308 | 309 | if key in materials.keys(): 310 | copy_key = duplicate.material(key) 311 | atom.materials_field = copy_key 312 | 313 | elif inspection == 'NODE_GROUPS': 314 | key = atom.node_groups_field 315 | node_groups = bpy.data.node_groups 316 | 317 | if key in node_groups.keys(): 318 | copy_key = duplicate.node_group(key) 319 | atom.node_groups_field = copy_key 320 | 321 | elif inspection == 'PARTICLES': 322 | key = atom.particles_field 323 | particles = bpy.data.particles 324 | 325 | if key in particles.keys(): 326 | copy_key = duplicate.particle(key) 327 | atom.particles_field = copy_key 328 | 329 | elif inspection == 'TEXTURES': 330 | key = atom.textures_field 331 | textures = bpy.data.textures 332 | 333 | if key in textures.keys(): 334 | copy_key = duplicate.texture(key) 335 | atom.textures_field = copy_key 336 | 337 | elif inspection == 'WORLDS': 338 | key = atom.worlds_field 339 | worlds = bpy.data.worlds 340 | 341 | if key in worlds.keys(): 342 | copy_key = duplicate.world(key) 343 | atom.worlds_field = copy_key 344 | 345 | return {'FINISHED'} 346 | 347 | 348 | # Atomic Data Manager Inspection Delete Operator 349 | class ATOMIC_OT_inspection_delete(bpy.types.Operator): 350 | """Forcibly remove this data-block from the project""" 351 | bl_idname = "atomic.inspection_delete" 352 | bl_label = "Delete Data-Block" 353 | 354 | def execute(self, context): 355 | atom = bpy.context.scene.atomic 356 | inspection = atom.active_inspection 357 | 358 | if inspection == 'COLLECTIONS': 359 | key = atom.collections_field 360 | collections = bpy.data.collections 361 | 362 | if key in collections.keys(): 363 | delete.collection(key) 364 | atom.collections_field = "" 365 | 366 | elif inspection == 'IMAGES': 367 | key = atom.images_field 368 | images = bpy.data.images 369 | 370 | if key in images.keys(): 371 | delete.image(key) 372 | atom.images_field = "" 373 | 374 | elif inspection == 'LIGHTS': 375 | key = atom.lights_field 376 | lights = bpy.data.lights 377 | 378 | if key in lights.keys(): 379 | delete.light(key) 380 | atom.lights_field = "" 381 | 382 | elif inspection == 'MATERIALS': 383 | key = atom.materials_field 384 | materials = bpy.data.materials 385 | 386 | if key in materials.keys(): 387 | delete.material(key) 388 | atom.materials_field = "" 389 | 390 | elif inspection == 'NODE_GROUPS': 391 | key = atom.node_groups_field 392 | node_groups = bpy.data.node_groups 393 | 394 | if key in node_groups.keys(): 395 | delete.node_group(key) 396 | atom.node_groups_field = "" 397 | 398 | elif inspection == 'PARTICLES': 399 | key = atom.particles_field 400 | particles = bpy.data.particles 401 | if key in particles.keys(): 402 | delete.particle(key) 403 | atom.particles_field = "" 404 | 405 | elif inspection == 'TEXTURES': 406 | key = atom.textures_field 407 | textures = bpy.data.textures 408 | 409 | if key in textures.keys(): 410 | delete.texture(key) 411 | atom.textures_field = "" 412 | 413 | elif inspection == 'WORLDS': 414 | key = atom.worlds_field 415 | worlds = bpy.data.worlds 416 | 417 | if key in worlds.keys(): 418 | delete.world(key) 419 | atom.worlds_field = "" 420 | 421 | return {'FINISHED'} 422 | 423 | 424 | reg_list = [ 425 | ATOMIC_OT_inspection_rename, 426 | ATOMIC_OT_inspection_replace, 427 | ATOMIC_OT_inspection_toggle_fake_user, 428 | ATOMIC_OT_inspection_duplicate, 429 | ATOMIC_OT_inspection_delete 430 | ] 431 | 432 | 433 | def register(): 434 | for item in reg_list: 435 | register_class(item) 436 | 437 | 438 | def unregister(): 439 | for item in reg_list: 440 | unregister_class(item) 441 | -------------------------------------------------------------------------------- /ops/main_ops.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the main operators found in the main panel of the 22 | Atomic Data Manager interface. This includes nuke, clean, undo, and the 23 | various selection operations. 24 | 25 | """ 26 | 27 | import bpy 28 | from bpy.utils import register_class 29 | from bpy.utils import unregister_class 30 | from atomic_data_manager.stats import unused 31 | from atomic_data_manager.ops.utils import clean 32 | from atomic_data_manager.ops.utils import nuke 33 | from atomic_data_manager.ui.utils import ui_layouts 34 | 35 | 36 | # Atomic Data Manager Nuke Operator 37 | class ATOMIC_OT_nuke(bpy.types.Operator): 38 | """Remove all data-blocks from the selected categories""" 39 | bl_idname = "atomic.nuke" 40 | bl_label = "CAUTION!" 41 | 42 | def draw(self, context): 43 | atom = bpy.context.scene.atomic 44 | layout = self.layout 45 | 46 | col = layout.column() 47 | col.label(text="Remove the following data-blocks?") 48 | 49 | # No Data Section 50 | if not (atom.collections or atom.images or atom.lights or 51 | atom.materials or atom.node_groups or atom.particles or 52 | atom.textures or atom.worlds): 53 | 54 | ui_layouts.box_list( 55 | layout=layout, 56 | ) 57 | 58 | # display when the main panel collections property is toggled 59 | if atom.collections: 60 | collections = sorted(bpy.data.collections.keys()) 61 | ui_layouts.box_list( 62 | layout=layout, 63 | title="Collections", 64 | items=collections, 65 | icon="OUTLINER_OB_GROUP_INSTANCE" 66 | ) 67 | 68 | # display when the main panel images property is toggled 69 | if atom.images: 70 | images = sorted(bpy.data.images.keys()) 71 | ui_layouts.box_list( 72 | layout=layout, 73 | title="Images", 74 | items=images, 75 | icon="IMAGE_DATA" 76 | ) 77 | 78 | # display when the main panel lights property is toggled 79 | if atom.lights: 80 | lights = sorted(bpy.data.lights.keys()) 81 | ui_layouts.box_list( 82 | layout=layout, 83 | title="Lights", 84 | items=lights, 85 | icon="OUTLINER_OB_LIGHT" 86 | ) 87 | 88 | # display when the main panel materials property is toggled 89 | if atom.materials: 90 | materials = sorted(bpy.data.materials.keys()) 91 | ui_layouts.box_list( 92 | layout=layout, 93 | title="Materials", 94 | items=materials, 95 | icon="MATERIAL" 96 | ) 97 | 98 | # display when the main panel node groups property is toggled 99 | if atom.node_groups: 100 | node_groups = sorted(bpy.data.node_groups.keys()) 101 | ui_layouts.box_list( 102 | layout=layout, 103 | title="Node Groups", 104 | items=node_groups, 105 | icon="NODETREE" 106 | ) 107 | 108 | # display when the main panel particle systems property is toggled 109 | if atom.particles: 110 | particles = sorted(bpy.data.particles.keys()) 111 | ui_layouts.box_list( 112 | layout=layout, 113 | title="Particle Systems", 114 | items=particles, 115 | icon="PARTICLES" 116 | ) 117 | 118 | # display when the main panel textures property is toggled 119 | if atom.textures: 120 | textures = sorted(bpy.data.textures.keys()) 121 | ui_layouts.box_list( 122 | layout=layout, 123 | title="Textures", 124 | items=textures, 125 | icon="TEXTURE" 126 | ) 127 | 128 | # display when the main panel worlds property is toggled 129 | if atom.worlds: 130 | worlds = sorted(bpy.data.worlds.keys()) 131 | ui_layouts.box_list( 132 | layout=layout, 133 | title="Worlds", 134 | items=worlds, 135 | icon="WORLD" 136 | ) 137 | 138 | row = layout.row() # extra spacing 139 | 140 | def execute(self, context): 141 | atom = bpy.context.scene.atomic 142 | 143 | if atom.collections: 144 | nuke.collections() 145 | 146 | if atom.images: 147 | nuke.images() 148 | 149 | if atom.lights: 150 | nuke.lights() 151 | 152 | if atom.materials: 153 | nuke.materials() 154 | 155 | if atom.node_groups: 156 | nuke.node_groups() 157 | 158 | if atom.particles: 159 | nuke.particles() 160 | 161 | if atom.textures: 162 | nuke.textures() 163 | 164 | if atom.worlds: 165 | nuke.worlds() 166 | 167 | bpy.ops.atomic.deselect_all() 168 | 169 | return {'FINISHED'} 170 | 171 | def invoke(self, context, event): 172 | wm = context.window_manager 173 | return wm.invoke_props_dialog(self) 174 | 175 | 176 | # Atomic Data Manager Clean Operator 177 | class ATOMIC_OT_clean(bpy.types.Operator): 178 | """Remove all unused data-blocks from the selected categories""" 179 | bl_idname = "atomic.clean" 180 | bl_label = "Clean" 181 | 182 | unused_collections = [] 183 | unused_images = [] 184 | unused_lights = [] 185 | unused_materials = [] 186 | unused_node_groups = [] 187 | unused_particles = [] 188 | unused_textures = [] 189 | unused_worlds = [] 190 | 191 | def draw(self, context): 192 | atom = bpy.context.scene.atomic 193 | layout = self.layout 194 | 195 | col = layout.column() 196 | col.label(text="Remove the following data-blocks?") 197 | 198 | # display if no main panel properties are toggled 199 | if not (atom.collections or atom.images or atom.lights or 200 | atom.materials or atom.node_groups or atom.particles 201 | or atom.textures or atom.worlds): 202 | 203 | ui_layouts.box_list( 204 | layout=layout, 205 | ) 206 | 207 | # display when the main panel collections property is toggled 208 | if atom.collections: 209 | ui_layouts.box_list( 210 | layout=layout, 211 | title="Collections", 212 | items=self.unused_collections, 213 | icon="OUTLINER_OB_GROUP_INSTANCE" 214 | ) 215 | 216 | # display when the main panel images property is toggled 217 | if atom.images: 218 | ui_layouts.box_list( 219 | layout=layout, 220 | title="Images", 221 | items=self.unused_images, 222 | icon="IMAGE_DATA" 223 | ) 224 | 225 | # display when the main panel lights property is toggled 226 | if atom.lights: 227 | ui_layouts.box_list( 228 | layout=layout, 229 | title="Lights", 230 | items=self.unused_lights, 231 | icon="OUTLINER_OB_LIGHT" 232 | ) 233 | 234 | # display when the main panel materials property is toggled 235 | if atom.materials: 236 | ui_layouts.box_list( 237 | layout=layout, 238 | title="Materials", 239 | items=self.unused_materials, 240 | icon="MATERIAL" 241 | ) 242 | 243 | # display when the main panel node groups property is toggled 244 | if atom.node_groups: 245 | ui_layouts.box_list( 246 | layout=layout, 247 | title="Node Groups", 248 | items=self.unused_node_groups, 249 | icon="NODETREE" 250 | ) 251 | 252 | # display when the main panel particle systems property is toggled 253 | if atom.particles: 254 | ui_layouts.box_list( 255 | layout=layout, 256 | title="Particle Systems", 257 | items=self.unused_particles, 258 | icon="PARTICLES" 259 | ) 260 | 261 | # display when the main panel textures property is toggled 262 | if atom.textures: 263 | textures = sorted(unused.textures_deep()) 264 | ui_layouts.box_list( 265 | layout=layout, 266 | title="Textures", 267 | items=textures, 268 | icon="TEXTURE" 269 | ) 270 | 271 | # display when the main panel worlds property is toggled 272 | if atom.worlds: 273 | ui_layouts.box_list( 274 | layout=layout, 275 | title="Worlds", 276 | items=self.unused_worlds, 277 | icon="WORLD" 278 | ) 279 | 280 | row = layout.row() # extra spacing 281 | 282 | def execute(self, context): 283 | atom = bpy.context.scene.atomic 284 | 285 | if atom.collections: 286 | clean.collections() 287 | 288 | if atom.images: 289 | clean.images() 290 | 291 | if atom.lights: 292 | clean.lights() 293 | 294 | if atom.materials: 295 | clean.materials() 296 | 297 | if atom.node_groups: 298 | clean.node_groups() 299 | 300 | if atom.particles: 301 | clean.particles() 302 | 303 | if atom.textures: 304 | clean.textures() 305 | 306 | if atom.worlds: 307 | clean.worlds() 308 | 309 | bpy.ops.atomic.deselect_all() 310 | 311 | return {'FINISHED'} 312 | 313 | def invoke(self, context, event): 314 | wm = context.window_manager 315 | atom = bpy.context.scene.atomic 316 | 317 | if atom.collections: 318 | self.unused_collections = unused.collections_deep() 319 | 320 | if atom.images: 321 | self.unused_images = unused.images_deep() 322 | 323 | if atom.lights: 324 | self.unused_lights = unused.lights_deep() 325 | 326 | if atom.materials: 327 | self.unused_materials = unused.materials_deep() 328 | 329 | if atom.node_groups: 330 | self.unused_node_groups = unused.node_groups_deep() 331 | 332 | if atom.particles: 333 | self.unused_particles = unused.particles_deep() 334 | 335 | if atom.textures: 336 | self.unused_textures = unused.textures_deep() 337 | 338 | if atom.worlds: 339 | self.unused_worlds = unused.worlds() 340 | 341 | return wm.invoke_props_dialog(self) 342 | 343 | 344 | # Atomic Data Manager Undo Operator 345 | class ATOMIC_OT_undo(bpy.types.Operator): 346 | """Undo the previous action""" 347 | bl_idname = "atomic.undo" 348 | bl_label = "Undo" 349 | 350 | def execute(self, context): 351 | bpy.ops.ed.undo() 352 | return {'FINISHED'} 353 | 354 | 355 | # Atomic Data Manager Smart Select Operator 356 | class ATOMIC_OT_smart_select(bpy.types.Operator): 357 | """Auto-select categories with unused data""" 358 | bl_idname = "atomic.smart_select" 359 | bl_label = "Smart Select" 360 | 361 | def execute(self, context): 362 | 363 | bpy.context.scene.atomic.collections = \ 364 | any(unused.collections_deep()) 365 | 366 | bpy.context.scene.atomic.images = \ 367 | any(unused.images_deep()) 368 | 369 | bpy.context.scene.atomic.lights = \ 370 | any(unused.lights_deep()) 371 | 372 | bpy.context.scene.atomic.materials = \ 373 | any(unused.materials_deep()) 374 | 375 | bpy.context.scene.atomic.node_groups = \ 376 | any(unused.node_groups_deep()) 377 | 378 | bpy.context.scene.atomic.particles = \ 379 | any(unused.particles_deep()) 380 | 381 | bpy.context.scene.atomic.textures = \ 382 | any(unused.textures_deep()) 383 | 384 | bpy.context.scene.atomic.worlds = \ 385 | any(unused.worlds()) 386 | 387 | return {'FINISHED'} 388 | 389 | 390 | # Atomic Data Manager Select All Operator 391 | class ATOMIC_OT_select_all(bpy.types.Operator): 392 | """Select all categories""" 393 | bl_idname = "atomic.select_all" 394 | bl_label = "Select All" 395 | 396 | def execute(self, context): 397 | bpy.context.scene.atomic.collections = True 398 | bpy.context.scene.atomic.images = True 399 | bpy.context.scene.atomic.lights = True 400 | bpy.context.scene.atomic.materials = True 401 | bpy.context.scene.atomic.node_groups = True 402 | bpy.context.scene.atomic.particles = True 403 | bpy.context.scene.atomic.textures = True 404 | bpy.context.scene.atomic.worlds = True 405 | return {'FINISHED'} 406 | 407 | 408 | # Atomic Data Manager Deselect All Operator 409 | class ATOMIC_OT_deselect_all(bpy.types.Operator): 410 | """Deselect all categories""" 411 | bl_idname = "atomic.deselect_all" 412 | bl_label = "Deselect All" 413 | 414 | def execute(self, context): 415 | bpy.context.scene.atomic.collections = False 416 | bpy.context.scene.atomic.images = False 417 | bpy.context.scene.atomic.lights = False 418 | bpy.context.scene.atomic.materials = False 419 | bpy.context.scene.atomic.node_groups = False 420 | bpy.context.scene.atomic.particles = False 421 | bpy.context.scene.atomic.textures = False 422 | bpy.context.scene.atomic.worlds = False 423 | 424 | return {'FINISHED'} 425 | 426 | 427 | reg_list = [ 428 | ATOMIC_OT_nuke, 429 | ATOMIC_OT_clean, 430 | ATOMIC_OT_undo, 431 | ATOMIC_OT_smart_select, 432 | ATOMIC_OT_select_all, 433 | ATOMIC_OT_deselect_all 434 | ] 435 | 436 | 437 | def register(): 438 | for item in reg_list: 439 | register_class(item) 440 | 441 | 442 | def unregister(): 443 | for item in reg_list: 444 | unregister_class(item) 445 | -------------------------------------------------------------------------------- /ops/missing_file_ops.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains operations for missing file handling. This includes 22 | the option to reload, remove, replace, and search for these missing files. 23 | 24 | It also contains the post-reload report dialog that appears after 25 | attempting to reload missing project files. 26 | 27 | # TODO: implement missing file replace and search once Blender fixes the 28 | # TODO: bugs with the file chooser not opening from a dialog 29 | 30 | """ 31 | 32 | import bpy 33 | from bpy.utils import register_class 34 | from bpy.utils import unregister_class 35 | from atomic_data_manager.stats import missing 36 | from atomic_data_manager.ui.utils import ui_layouts 37 | 38 | 39 | # Atomic Data Manager Reload Missing Files Operator 40 | class ATOMIC_OT_reload_missing(bpy.types.Operator): 41 | """Reload missing files""" 42 | bl_idname = "atomic.reload_missing" 43 | bl_label = "Reload Missing Files" 44 | 45 | def execute(self, context): 46 | # reload images 47 | for image in bpy.data.images: 48 | image.reload() 49 | 50 | # reload libraries 51 | for library in bpy.data.libraries: 52 | library.reload() 53 | 54 | # call reload report 55 | bpy.ops.atomic.reload_report('INVOKE_DEFAULT') 56 | return {'FINISHED'} 57 | 58 | 59 | # Atomic Data Manager Reload Missing Files Report Operator 60 | class ATOMIC_OT_reload_report(bpy.types.Operator): 61 | """Reload report for missing files""" 62 | bl_idname = "atomic.reload_report" 63 | bl_label = "Missing File Reload Report" 64 | 65 | def draw(self, context): 66 | layout = self.layout 67 | missing_images = missing.images() 68 | missing_libraries = missing.libraries() 69 | 70 | if missing_images or missing_libraries: 71 | row = layout.row() 72 | row.label( 73 | text="Atomic was unable to reload the following files:" 74 | ) 75 | 76 | if missing_images: 77 | ui_layouts.box_list( 78 | layout=self.layout, 79 | items=missing_images, 80 | icon='IMAGE_DATA', 81 | columns=2 82 | ) 83 | 84 | if missing_libraries: 85 | ui_layouts.box_list( 86 | layout=self.layout, 87 | items=missing_images, 88 | icon='LIBRARY_DATA_DIRECT', 89 | columns=2 90 | ) 91 | 92 | else: 93 | row = layout.row() 94 | row.label(text="All files successfully reloaded!") 95 | 96 | row = layout.row() # extra space 97 | 98 | def execute(self, context): 99 | return {'FINISHED'} 100 | 101 | def invoke(self, context, event): 102 | wm = context.window_manager 103 | return wm.invoke_props_dialog(self) 104 | 105 | 106 | # Atomic Data Manager Remove Missing Files Operator 107 | class ATOMIC_OT_remove_missing(bpy.types.Operator): 108 | """Remove all missing files from this project""" 109 | bl_idname = "atomic.remove_missing" 110 | bl_label = "Remove Missing Files" 111 | 112 | def draw(self, context): 113 | layout = self.layout 114 | row = layout.row() 115 | row.label(text="Remove the following data-blocks?") 116 | 117 | ui_layouts.box_list( 118 | layout=layout, 119 | items=missing.images(), 120 | icon="IMAGE_DATA", 121 | columns=2 122 | ) 123 | 124 | row = layout.row() # extra space 125 | 126 | def execute(self, context): 127 | for image_key in missing.images(): 128 | bpy.data.images.remove(bpy.data.images[image_key]) 129 | 130 | return {'FINISHED'} 131 | 132 | def invoke(self, context, event): 133 | wm = context.window_manager 134 | return wm.invoke_props_dialog(self) 135 | 136 | 137 | # TODO: Implement search for missing once file browser bugs are fixed 138 | # Atomic Data Manager Search for Missing Files Operator 139 | class ATOMIC_OT_search_missing(bpy.types.Operator): 140 | """Search a specified directory for the missing files""" 141 | bl_idname = "atomic.search_missing" 142 | bl_label = "Search for Missing Files" 143 | 144 | def draw(self, context): 145 | layout = self.layout 146 | 147 | row = layout.row() 148 | row.label(text="Unsupported Operation!") 149 | 150 | def execute(self, context): 151 | return {'FINISHED'} 152 | 153 | def invoke(self, context, event): 154 | wm = context.window_manager 155 | return wm.invoke_props_dialog(self) 156 | 157 | 158 | # TODO: Implement replace missing once file browser bugs are fixed 159 | # Atomic Data Manager Replace Missing Files Operator 160 | class ATOMIC_OT_replace_missing(bpy.types.Operator): 161 | """Replace each missing file with a new file""" 162 | bl_idname = "atomic.replace_missing" 163 | bl_label = "Replace Missing Files" 164 | 165 | def draw(self, context): 166 | layout = self.layout 167 | 168 | row = layout.row() 169 | row.label(text="Unsupported Operation!") 170 | 171 | def execute(self, context): 172 | return {'FINISHED'} 173 | 174 | def invoke(self, context, event): 175 | wm = context.window_manager 176 | return wm.invoke_props_dialog(self) 177 | 178 | 179 | reg_list = [ 180 | ATOMIC_OT_reload_missing, 181 | ATOMIC_OT_reload_report, 182 | ATOMIC_OT_search_missing, 183 | ATOMIC_OT_replace_missing, 184 | ATOMIC_OT_remove_missing 185 | ] 186 | 187 | 188 | def register(): 189 | for item in reg_list: 190 | register_class(item) 191 | 192 | 193 | def unregister(): 194 | for item in reg_list: 195 | unregister_class(item) 196 | -------------------------------------------------------------------------------- /ops/support_me_ops.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | 20 | --- 21 | 22 | This file contains the operator for opening the Remington Creative 23 | support page in the web browser. 24 | 25 | 26 | """ 27 | 28 | import bpy 29 | import webbrowser 30 | from bpy.utils import register_class 31 | from bpy.utils import unregister_class 32 | 33 | 34 | # Atomic Data Manager Open Support Me Operator 35 | class ATOMIC_OT_open_support_me(bpy.types.Operator): 36 | """Opens the Remington Creative \"Support Me\" webpage""" 37 | bl_idname = "atomic.open_support_me" 38 | bl_label = "Support Me" 39 | 40 | def execute(self, context): 41 | webbrowser.open("https://remingtoncreative.com/support/") 42 | return {'FINISHED'} 43 | 44 | 45 | reg_list = [ATOMIC_OT_open_support_me] 46 | 47 | 48 | def register(): 49 | for cls in reg_list: 50 | register_class(cls) 51 | 52 | 53 | def unregister(): 54 | for cls in reg_list: 55 | unregister_class(cls) 56 | -------------------------------------------------------------------------------- /ops/utils/clean.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions for cleaning out specific data categories. 22 | 23 | """ 24 | 25 | import bpy 26 | from atomic_data_manager.stats import unused 27 | 28 | 29 | def collections(): 30 | # removes all unused collections from the project 31 | for collection_key in unused.collections_deep(): 32 | bpy.data.collections.remove(bpy.data.collections[collection_key]) 33 | 34 | 35 | def images(): 36 | # removes all unused images from the project 37 | for image_key in unused.images_deep(): 38 | bpy.data.images.remove(bpy.data.images[image_key]) 39 | 40 | 41 | def lights(): 42 | # removes all unused lights from the project 43 | for light_key in unused.lights_deep(): 44 | bpy.data.lights.remove(bpy.data.lights[light_key]) 45 | 46 | 47 | def materials(): 48 | # removes all unused materials from the project 49 | for light_key in unused.materials_deep(): 50 | bpy.data.materials.remove(bpy.data.materials[light_key]) 51 | 52 | 53 | def node_groups(): 54 | # removes all unused node groups from the project 55 | for node_group_key in unused.node_groups_deep(): 56 | bpy.data.node_groups.remove(bpy.data.node_groups[node_group_key]) 57 | 58 | 59 | def particles(): 60 | # removes all unused particle systems from the project 61 | for particle_key in unused.particles_deep(): 62 | bpy.data.particles.remove(bpy.data.particles[particle_key]) 63 | 64 | 65 | def textures(): 66 | # removes all unused textures from the project 67 | for texture_key in unused.textures_deep(): 68 | bpy.data.textures.remove(bpy.data.textures[texture_key]) 69 | 70 | 71 | def worlds(): 72 | # removes all unused worlds from the project 73 | for world_key in unused.worlds(): 74 | bpy.data.worlds.remove(bpy.data.worlds[world_key]) 75 | -------------------------------------------------------------------------------- /ops/utils/delete.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions for deleting individual data-blocks from 22 | Atomic's inspection inteface. 23 | 24 | """ 25 | 26 | import bpy 27 | 28 | 29 | def delete_datablock(data, key): 30 | # deletes a specific data-block from a set of data 31 | data.remove(data[key]) 32 | 33 | 34 | def collection(key): 35 | # removes a specific collection 36 | delete_datablock(bpy.data.collections, key) 37 | 38 | 39 | def image(key): 40 | # removes a specific image 41 | delete_datablock(bpy.data.images, key) 42 | 43 | 44 | def light(key): 45 | # removes a specific light 46 | delete_datablock(bpy.data.lights, key) 47 | 48 | 49 | def material(key): 50 | # removes a specific material 51 | delete_datablock(bpy.data.materials, key) 52 | 53 | 54 | def node_group(key): 55 | # removes a specific node group 56 | delete_datablock(bpy.data.node_groups, key) 57 | 58 | 59 | def particle(key): 60 | # removes a specific particle system 61 | delete_datablock(bpy.data.particles, key) 62 | 63 | 64 | def texture(key): 65 | # removes a specific texture 66 | delete_datablock(bpy.data.textures, key) 67 | 68 | 69 | def world(key): 70 | # removes a specific world 71 | delete_datablock(bpy.data.worlds, key) 72 | -------------------------------------------------------------------------------- /ops/utils/duplicate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions for duplicating data-blocks from Atomic's 22 | inspection interface. 23 | 24 | """ 25 | 26 | import bpy 27 | 28 | 29 | def duplicate_data(data, key): 30 | # creates a copy of the specified data-block and returns its key 31 | return data[key].copy().name 32 | 33 | 34 | def collection(key): 35 | # creates of copy of the specified collection and places it under the 36 | # scene collection 37 | collections = bpy.data.collections 38 | scene_collection = bpy.context.scene.collection 39 | 40 | copy_key = duplicate_data(collections, key) 41 | scene_collection.children.link(collections[copy_key]) 42 | return copy_key 43 | 44 | 45 | def image(key): 46 | # creates of copy of the specified image 47 | return duplicate_data(bpy.data.images, key) 48 | 49 | 50 | def light(key): 51 | # creates of copy of the specified light 52 | return duplicate_data(bpy.data.lights, key) 53 | 54 | 55 | def material(key): 56 | # creates of copy of the specified material 57 | return duplicate_data(bpy.data.materials, key) 58 | 59 | 60 | def node_group(key): 61 | # creates of copy of the specified node group 62 | return duplicate_data(bpy.data.node_groups, key) 63 | 64 | 65 | def particle(key): 66 | # creates of copy of the specified particle 67 | return duplicate_data(bpy.data.particles, key) 68 | 69 | 70 | def texture(key): 71 | # creates of copy of the specified texture 72 | return duplicate_data(bpy.data.textures, key) 73 | 74 | 75 | def world(key): 76 | # creates of copy of the specified world 77 | return duplicate_data(bpy.data.worlds, key) 78 | -------------------------------------------------------------------------------- /ops/utils/nuke.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions for removing all data-blocks from specified 22 | data categories. 23 | 24 | """ 25 | 26 | import bpy 27 | 28 | 29 | def nuke_data(data): 30 | # removes all data-blocks from the indicated set of data 31 | for key in data.keys(): 32 | data.remove(data[key]) 33 | 34 | 35 | def collections(): 36 | # removes all collections from the project 37 | nuke_data(bpy.data.collections) 38 | 39 | 40 | def images(): 41 | # removes all images from the project 42 | nuke_data(bpy.data.images) 43 | 44 | 45 | def lights(): 46 | # removes all lights from the project 47 | nuke_data(bpy.data.lights) 48 | 49 | 50 | def materials(): 51 | # removes all materials from the project 52 | nuke_data(bpy.data.materials) 53 | 54 | 55 | def node_groups(): 56 | # removes all node groups from the project 57 | nuke_data(bpy.data.node_groups) 58 | 59 | 60 | def particles(): 61 | # removes all particle systems from the project 62 | nuke_data(bpy.data.particles) 63 | 64 | 65 | def textures(): 66 | # removes all textures from the project 67 | nuke_data(bpy.data.textures) 68 | 69 | 70 | def worlds(): 71 | # removes all worlds from the project 72 | nuke_data(bpy.data.worlds) 73 | -------------------------------------------------------------------------------- /stats/count.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions that count quantities of various sets of data. 22 | 23 | """ 24 | 25 | import bpy 26 | from atomic_data_manager.stats import unused 27 | from atomic_data_manager.stats import unnamed 28 | from atomic_data_manager.stats import missing 29 | 30 | 31 | def collections(): 32 | # returns the number of collections in the project 33 | 34 | return len(bpy.data.collections) 35 | 36 | 37 | def collections_unused(): 38 | # returns the number of unused collections in the project 39 | 40 | return len(unused.collections_shallow()) 41 | 42 | 43 | def collections_unnamed(): 44 | # returns the number of unnamed collections in the project 45 | 46 | return len(unnamed.collections()) 47 | 48 | 49 | def images(): 50 | # returns the number of images in the project 51 | 52 | return len(bpy.data.images) 53 | 54 | 55 | def images_unused(): 56 | # returns the number of unused images in the project 57 | 58 | return len(unused.images_shallow()) 59 | 60 | 61 | def images_unnamed(): 62 | # returns the number of unnamed images in the project 63 | 64 | return len(unnamed.images()) 65 | 66 | 67 | def images_missing(): 68 | # returns the number of missing images in the project 69 | 70 | return len(missing.images()) 71 | 72 | 73 | def lights(): 74 | # returns the number of lights in the project 75 | 76 | return len(bpy.data.lights) 77 | 78 | 79 | def lights_unused(): 80 | # returns the number of unused lights in the project 81 | 82 | return len(unused.lights_shallow()) 83 | 84 | 85 | def lights_unnamed(): 86 | # returns the number of unnamed lights in the project 87 | 88 | return len(unnamed.lights()) 89 | 90 | 91 | def materials(): 92 | # returns the number of materials in the project 93 | 94 | return len(bpy.data.materials) 95 | 96 | 97 | def materials_unused(): 98 | # returns the number of unused materials in the project 99 | 100 | return len(unused.materials_shallow()) 101 | 102 | 103 | def materials_unnamed(): 104 | # returns the number of unnamed materials in the project 105 | 106 | return len(unnamed.materials()) 107 | 108 | 109 | def node_groups(): 110 | # returns the number of node groups in the project 111 | 112 | return len(bpy.data.node_groups) 113 | 114 | 115 | def node_groups_unused(): 116 | # returns the number of unused node groups in the project 117 | 118 | return len(unused.node_groups_shallow()) 119 | 120 | 121 | def node_groups_unnamed(): 122 | # returns the number of unnamed node groups in the project 123 | 124 | return len(unnamed.node_groups()) 125 | 126 | 127 | def objects(): 128 | # returns the number of objects in the project 129 | 130 | return len(bpy.data.objects) 131 | 132 | 133 | def objects_unnamed(): 134 | # returns the number of unnamed objects in the project 135 | 136 | return len(unnamed.objects()) 137 | 138 | 139 | def particles(): 140 | # returns the number of particles in the project 141 | 142 | return len(bpy.data.particles) 143 | 144 | 145 | def particles_unused(): 146 | # returns the number of unused particles in the project 147 | 148 | return len(unused.particles_shallow()) 149 | 150 | 151 | def particles_unnamed(): 152 | # returns the number of unnamed particle systems in the project 153 | 154 | return len(unnamed.particles()) 155 | 156 | 157 | def textures(): 158 | # returns the number of textures in the project 159 | 160 | return len(bpy.data.textures) 161 | 162 | 163 | def textures_unused(): 164 | # returns the number of unused textures in the project 165 | 166 | return len(unused.textures_shallow()) 167 | 168 | 169 | def textures_unnamed(): 170 | # returns the number of unnamed textures in the project 171 | 172 | return len(unnamed.textures()) 173 | 174 | 175 | def worlds(): 176 | # returns the number of worlds in the project 177 | 178 | return len(bpy.data.worlds) 179 | 180 | 181 | def worlds_unused(): 182 | # returns the number of unused worlds in the project 183 | 184 | return len(unused.worlds()) 185 | 186 | 187 | def worlds_unnamed(): 188 | # returns the number of unnamed worlds in the project 189 | 190 | return len(unnamed.worlds()) 191 | -------------------------------------------------------------------------------- /stats/misc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains miscellaneous statistics functions. 22 | 23 | """ 24 | 25 | import bpy 26 | import os 27 | 28 | 29 | def blend_size(): 30 | # returns the size of the current Blender file as a string 31 | 32 | filepath = bpy.data.filepath 33 | size_bytes = os.stat(filepath).st_size if filepath != '' else -1 34 | 35 | kilobyte = 1024 # bytes 36 | megabyte = 1048576 # bytes 37 | gigabyte = 1073741824 # bytes 38 | 39 | if 0 <= size_bytes < kilobyte: 40 | size_scaled = "{:.1f} B".format(size_bytes) 41 | elif kilobyte <= size_bytes < megabyte: 42 | size_scaled = "{:.1f} KB".format(size_bytes / kilobyte) 43 | elif megabyte <= size_bytes < gigabyte: 44 | size_scaled = "{:.1f} MB".format(size_bytes / megabyte) 45 | elif size_bytes >= gigabyte: 46 | size_scaled = "{:.1f} GB".format(size_bytes / gigabyte) 47 | else: 48 | size_scaled = "No Data!" 49 | 50 | return size_scaled 51 | -------------------------------------------------------------------------------- /stats/missing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions that detect missing files in the Blender 22 | project. 23 | 24 | """ 25 | 26 | import bpy 27 | import os 28 | 29 | 30 | def get_missing(data): 31 | # returns a list of keys of unpacked data-blocks with non-existent 32 | # filepaths 33 | 34 | missing = [] 35 | 36 | # list of keys that should not be flagged 37 | do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"] 38 | 39 | for datablock in data: 40 | 41 | # the absolute path to our data-block 42 | abspath = bpy.path.abspath(datablock.filepath) 43 | 44 | # if data-block is not packed and has an invalid filepath 45 | if not datablock.packed_files and not os.path.isfile(abspath): 46 | 47 | # if data-block is not in our do not flag list 48 | # append it to the missing data list 49 | if datablock.name not in do_not_flag: 50 | missing.append(datablock.name) 51 | 52 | # if data-block is packed but it does not have a filepath 53 | elif datablock.packed_files and not abspath: 54 | 55 | # if data-block is not in our do not flag list 56 | # append it to the missing data list 57 | if datablock.name not in do_not_flag: 58 | missing.append(datablock.name) 59 | 60 | return missing 61 | 62 | 63 | def images(): 64 | # returns a list of keys of images with a non-existent filepath 65 | return get_missing(bpy.data.images) 66 | 67 | 68 | def libraries(): 69 | # returns a list of keys of libraries with a non-existent filepath 70 | return get_missing(bpy.data.libraries) 71 | -------------------------------------------------------------------------------- /stats/unnamed.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions that detect unnamed data-blocks in the 22 | Blender project. 23 | 24 | """ 25 | 26 | import bpy 27 | import re 28 | 29 | 30 | def collections(): 31 | # returns the keys of all unnamed collections in the project 32 | unnamed = [] 33 | 34 | for collection in bpy.data.collections: 35 | if re.match(r'.*\.\d\d\d$', collection.name) or \ 36 | collection.name.startswith("Collection"): 37 | unnamed.append(collection.name) 38 | 39 | return unnamed 40 | 41 | 42 | def images(): 43 | # returns the keys of all unnamed images in the project 44 | unnamed = [] 45 | 46 | for image in bpy.data.images: 47 | if re.match(r'.*\.\d\d\d$', image.name) or \ 48 | image.name.startswith("Untitled"): 49 | unnamed.append(image.name) 50 | 51 | return unnamed 52 | 53 | 54 | def lights(): 55 | # returns the keys of all unnamed lights in the project 56 | unnamed = [] 57 | 58 | for light in bpy.data.lights: 59 | if re.match(r'.*\.\d\d\d$', light.name) or \ 60 | light.name.startswith("Light"): 61 | unnamed.append(light.name) 62 | 63 | return unnamed 64 | 65 | 66 | def materials(): 67 | # returns the keys of all unnamed materials in the project 68 | unnamed = [] 69 | 70 | for material in bpy.data.lights: 71 | if re.match(r'.*\.\d\d\d$', material.name) or \ 72 | material.name.startswith("Material"): 73 | unnamed.append(material.name) 74 | 75 | return unnamed 76 | 77 | 78 | def objects(): 79 | # returns the keys of all unnamed materials in the project 80 | # NOTE: lists of default names must be tuples! 81 | 82 | # the default names all curve objects 83 | curve_names = ( 84 | "BezierCircle", 85 | "BezierCurve", 86 | "NurbsCircle", 87 | "NurbsCurve", 88 | "NurbsPath" 89 | ) 90 | 91 | # the default names of all grease pencil objects 92 | gpencil_names = ( 93 | "GPencil", 94 | "Stroke" 95 | ) 96 | 97 | # the default names of all light objects 98 | light_names = ( 99 | "Area", 100 | "Light", 101 | "Point", 102 | "Spot", 103 | "Sun" 104 | ) 105 | 106 | # the default names of all light probe objects 107 | lprobe_names = ( 108 | "IrradianceVolume", 109 | "ReflectionCubemap", 110 | "ReflectionPlane" 111 | ) 112 | 113 | # the default names of all mesh objects 114 | mesh_names = ( 115 | "Circle", 116 | "Cone", 117 | "Cube", 118 | "Cylinder", 119 | "Grid", 120 | "Icosphere", 121 | "Plane", 122 | "Sphere", 123 | "Torus" 124 | ) 125 | 126 | # the default names of all miscellaneous objects 127 | misc_names = ( 128 | "Mball", 129 | "Text", 130 | "Armature", 131 | "Lattice", 132 | "Empty", 133 | "Camera", 134 | "Speaker", 135 | "Field" 136 | ) 137 | 138 | # the default names of all nurbs objects 139 | nurbs_names = ( 140 | "SurfCircle", 141 | "SurfCurve", 142 | "SurfPatch", 143 | "SurfTorus", 144 | "Surface" 145 | ) 146 | 147 | # the default names of all objects compiled into one tuple 148 | default_obj_names = \ 149 | curve_names + gpencil_names + light_names + lprobe_names + \ 150 | mesh_names + misc_names + nurbs_names 151 | 152 | unnamed = [] 153 | 154 | for obj in bpy.data.objects: 155 | if re.match(r'.*\.\d\d\d$', obj.name) or \ 156 | obj.name.startswith(default_obj_names): 157 | unnamed.append(obj.name) 158 | 159 | return unnamed 160 | 161 | 162 | def node_groups(): 163 | # returns the keys of all unnamed node groups in the project 164 | unnamed = [] 165 | 166 | for node_group in bpy.data.node_groups: 167 | if re.match(r'.*\.\d\d\d$', node_group.name) or \ 168 | node_group.name.startswith("NodeGroup"): 169 | unnamed.append(node_group.name) 170 | 171 | return unnamed 172 | 173 | 174 | def particles(): 175 | # returns the keys of all unnamed particle systems in the project 176 | unnamed = [] 177 | 178 | for particle in bpy.data.particles: 179 | if re.match(r'.*\.\d\d\d$', particle.name) or \ 180 | particle.name.startswith("ParticleSettings"): 181 | unnamed.append(particle.name) 182 | 183 | return unnamed 184 | 185 | 186 | def textures(): 187 | # returns the keys of all unnamed textures in the project 188 | unnamed = [] 189 | 190 | for texture in bpy.data.textures: 191 | if re.match(r'.*\.\d\d\d$', texture.name) or \ 192 | texture.name.startswith("Texture"): 193 | unnamed.append(texture.name) 194 | 195 | return unnamed 196 | 197 | 198 | def worlds(): 199 | # returns the keys of all unnamed worlds in the project 200 | unnamed = [] 201 | 202 | for world in bpy.data.worlds: 203 | if re.match(r'.*\.\d\d\d$', world.name) or \ 204 | world.name.startswith("World"): 205 | unnamed.append(world.name) 206 | 207 | return unnamed -------------------------------------------------------------------------------- /stats/unused.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions that detect data-blocks that have no users, 22 | as determined by stats.users.py 23 | 24 | """ 25 | 26 | import bpy 27 | from atomic_data_manager import config 28 | from atomic_data_manager.stats import users 29 | 30 | 31 | def shallow(data): 32 | # returns a list of keys of unused data-blocks in the data that may be 33 | # incomplete, but is significantly faster than doing a deep search 34 | 35 | unused = [] 36 | 37 | for datablock in data: 38 | 39 | # if data-block has no users or if it has a fake user and 40 | # ignore fake users is enabled 41 | if datablock.users == 0 or (datablock.users == 1 and 42 | datablock.use_fake_user and 43 | config.include_fake_users): 44 | unused.append(datablock.name) 45 | 46 | return unused 47 | 48 | 49 | def collections_deep(): 50 | # returns a full list of keys of unused collections 51 | 52 | unused = [] 53 | 54 | for collection in bpy.data.collections: 55 | if not users.collection_all(collection.name): 56 | unused.append(collection.name) 57 | 58 | return unused 59 | 60 | 61 | def collections_shallow(): 62 | # returns a list of keys of unused collections that may be 63 | # incomplete, but is significantly faster. 64 | 65 | unused = [] 66 | 67 | for collection in bpy.data.collections: 68 | if not (collection.objects or collection.children): 69 | unused.append(collection.name) 70 | 71 | return unused 72 | 73 | 74 | def images_deep(): 75 | # returns a full list of keys of unused images 76 | 77 | unused = [] 78 | 79 | # a list of image keys that should not be flagged as unused 80 | # this list also exists in images_shallow() 81 | do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"] 82 | 83 | for image in bpy.data.images: 84 | if not users.image_all(image.name): 85 | 86 | # check if image has a fake user or if ignore fake users 87 | # is enabled 88 | if not image.use_fake_user or config.include_fake_users: 89 | 90 | # if image is not in our do not flag list 91 | if image.name not in do_not_flag: 92 | unused.append(image.name) 93 | 94 | return unused 95 | 96 | 97 | def images_shallow(): 98 | # returns a list of keys of unused images that may be 99 | # incomplete, but is significantly faster than doing a deep search 100 | 101 | unused_images = shallow(bpy.data.images) 102 | 103 | # a list of image keys that should not be flagged as unused 104 | # this list also exists in images_deep() 105 | do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"] 106 | 107 | # remove do not flag keys from unused images 108 | for key in do_not_flag: 109 | if key in unused_images: 110 | unused_images.remove(key) 111 | 112 | return unused_images 113 | 114 | 115 | def lights_deep(): 116 | # returns a list of keys of unused lights 117 | 118 | unused = [] 119 | 120 | for light in bpy.data.lights: 121 | if not users.light_all(light.name): 122 | 123 | # check if light has a fake user or if ignore fake users 124 | # is enabled 125 | if not light.use_fake_user or config.include_fake_users: 126 | unused.append(light.name) 127 | 128 | return unused 129 | 130 | 131 | def lights_shallow(): 132 | # returns a list of keys of unused lights that may be 133 | # incomplete, but is significantly faster than doing a deep search 134 | 135 | return shallow(bpy.data.lights) 136 | 137 | 138 | def materials_deep(): 139 | # returns a list of keys of unused materials 140 | 141 | unused = [] 142 | 143 | for material in bpy.data.materials: 144 | if not users.material_all(material.name): 145 | 146 | # check if material has a fake user or if ignore fake users 147 | # is enabled 148 | if not material.use_fake_user or config.include_fake_users: 149 | unused.append(material.name) 150 | 151 | return unused 152 | 153 | 154 | def materials_shallow(): 155 | # returns a list of keys of unused material that may be 156 | # incomplete, but is significantly faster than doing a deep search 157 | 158 | return shallow(bpy.data.materials) 159 | 160 | 161 | def node_groups_deep(): 162 | # returns a list of keys of unused node_groups 163 | 164 | unused = [] 165 | 166 | for node_group in bpy.data.node_groups: 167 | if not users.node_group_all(node_group.name): 168 | 169 | # check if node group has a fake user or if ignore fake users 170 | # is enabled 171 | if not node_group.use_fake_user or config.include_fake_users: 172 | unused.append(node_group.name) 173 | 174 | return unused 175 | 176 | 177 | def node_groups_shallow(): 178 | # returns a list of keys of unused node groups that may be 179 | # incomplete, but is significantly faster than doing a deep search 180 | 181 | return shallow(bpy.data.node_groups) 182 | 183 | 184 | def particles_deep(): 185 | # returns a list of keys of unused particle systems 186 | 187 | unused = [] 188 | 189 | for particle in bpy.data.particles: 190 | if not users.particle_all(particle.name): 191 | 192 | # check if particle system has a fake user or if ignore fake 193 | # users is enabled 194 | if not particle.use_fake_user or config.include_fake_users: 195 | unused.append(particle.name) 196 | 197 | return unused 198 | 199 | 200 | def particles_shallow(): 201 | # returns a list of keys of unused particle systems that may be 202 | # incomplete, but is significantly faster than doing a deep search 203 | 204 | return shallow(bpy.data.particles) 205 | 206 | 207 | def textures_deep(): 208 | # returns a list of keys of unused textures 209 | 210 | unused = [] 211 | 212 | for texture in bpy.data.textures: 213 | if not users.texture_all(texture.name): 214 | 215 | # check if texture has a fake user or if ignore fake users 216 | # is enabled 217 | if not texture.use_fake_user or config.include_fake_users: 218 | unused.append(texture.name) 219 | 220 | return unused 221 | 222 | 223 | def textures_shallow(): 224 | # returns a list of keys of unused textures that may be 225 | # incomplete, but is significantly faster than doing a deep search 226 | 227 | return shallow(bpy.data.textures) 228 | 229 | 230 | def worlds(): 231 | # returns a full list of keys of unused worlds 232 | 233 | unused = [] 234 | 235 | for world in bpy.data.worlds: 236 | 237 | # if data-block has no users or if it has a fake user and 238 | # ignore fake users is enabled 239 | if world.users == 0 or (world.users == 1 and 240 | world.use_fake_user and 241 | config.include_fake_users): 242 | unused.append(world.name) 243 | 244 | return unused 245 | -------------------------------------------------------------------------------- /stats/users.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains functions that return the keys of data-blocks that 22 | use other data-blocks. 23 | 24 | They are titled as such that the first part of the function name is the 25 | type of the data being passed in and the second part of the function name 26 | is the users of that type. 27 | 28 | e.g. If you were searching for all of the places where an image is used in 29 | a material would be searching for the image_materials() function. 30 | 31 | """ 32 | 33 | import bpy 34 | 35 | 36 | def collection_all(collection_key): 37 | # returns a list of keys of every data-block that uses this collection 38 | 39 | return collection_cameras(collection_key) + \ 40 | collection_children(collection_key) + \ 41 | collection_lights(collection_key) + \ 42 | collection_meshes(collection_key) + \ 43 | collection_others(collection_key) 44 | 45 | 46 | def collection_cameras(collection_key): 47 | # recursively returns a list of camera object keys that are in the 48 | # collection and its child collections 49 | 50 | users = [] 51 | collection = bpy.data.collections[collection_key] 52 | 53 | # append all camera objects in our collection 54 | for obj in collection.objects: 55 | if obj.type == 'CAMERA': 56 | users.append(obj.name) 57 | 58 | # list of all child collections in our collection 59 | children = collection_children(collection_key) 60 | 61 | # append all camera objects from the child collections 62 | for child in children: 63 | for obj in bpy.data.collections[child].objects: 64 | if obj.type == 'CAMERA': 65 | users.append(obj.name) 66 | 67 | return distinct(users) 68 | 69 | 70 | def collection_children(collection_key): 71 | # returns a list of all child collections under the specified 72 | # collection using recursive functions 73 | 74 | collection = bpy.data.collections[collection_key] 75 | 76 | children = collection_children_recursive(collection_key) 77 | children.remove(collection.name) 78 | 79 | return children 80 | 81 | 82 | def collection_children_recursive(collection_key): 83 | # recursively returns a list of all child collections under the 84 | # specified collection including the collection itself 85 | 86 | collection = bpy.data.collections[collection_key] 87 | 88 | # base case 89 | if not collection.children: 90 | return [collection.name] 91 | 92 | # recursion case 93 | else: 94 | children = [] 95 | for child in collection.children: 96 | children += collection_children(child.name) 97 | children.append(collection.name) 98 | return children 99 | 100 | 101 | def collection_lights(collection_key): 102 | # returns a list of light object keys that are in the collection 103 | 104 | users = [] 105 | collection = bpy.data.collections[collection_key] 106 | 107 | # append all light objects in our collection 108 | for obj in collection.objects: 109 | if obj.type == 'LIGHT': 110 | users.append(obj.name) 111 | 112 | # list of all child collections in our collection 113 | children = collection_children(collection_key) 114 | 115 | # append all light objects from the child collections 116 | for child in children: 117 | for obj in bpy.data.collections[child].objects: 118 | if obj.type == 'LIGHT': 119 | users.append(obj.name) 120 | 121 | return distinct(users) 122 | 123 | 124 | def collection_meshes(collection_key): 125 | # returns a list of mesh object keys that are in the collection 126 | 127 | users = [] 128 | collection = bpy.data.collections[collection_key] 129 | 130 | # append all mesh objects in our collection and from child 131 | # collections 132 | for obj in collection.all_objects: 133 | if obj.type == 'MESH': 134 | users.append(obj.name) 135 | 136 | return distinct(users) 137 | 138 | 139 | def collection_others(collection_key): 140 | # returns a list of other object keys that are in the collection 141 | # NOTE: excludes cameras, lights, and meshes 142 | 143 | users = [] 144 | collection = bpy.data.collections[collection_key] 145 | 146 | # object types to exclude from this search 147 | excluded_types = ['CAMERA', 'LIGHT', 'MESH'] 148 | 149 | # append all other objects in our collection and from child 150 | # collections 151 | for obj in collection.all_objects: 152 | if obj.type not in excluded_types: 153 | users.append(obj.name) 154 | 155 | return distinct(users) 156 | 157 | 158 | def image_all(image_key): 159 | # returns a list of keys of every data-block that uses this image 160 | 161 | return image_compositors(image_key) + \ 162 | image_materials(image_key) + \ 163 | image_node_groups(image_key) + \ 164 | image_textures(image_key) + \ 165 | image_worlds(image_key) 166 | 167 | 168 | def image_compositors(image_key): 169 | # returns a list containing "Compositor" if the image is used in 170 | # the scene's compositor 171 | 172 | users = [] 173 | image = bpy.data.images[image_key] 174 | 175 | # a list of node groups that use our image 176 | node_group_users = image_node_groups(image_key) 177 | 178 | # if our compositor uses nodes and has a valid node tree 179 | if bpy.context.scene.use_nodes and bpy.context.scene.node_tree: 180 | 181 | # check each node in the compositor 182 | for node in bpy.context.scene.node_tree.nodes: 183 | 184 | # if the node is an image node with a valid image 185 | if hasattr(node, 'image') and node.image: 186 | 187 | # if the node's image is our image 188 | if node.image.name == image.name: 189 | users.append("Compositor") 190 | 191 | # if the node is a group node with a valid node tree 192 | elif hasattr(node, 'node_tree') and node.node_tree: 193 | 194 | # if the node tree's name is in our list of node group 195 | # users 196 | if node.node_tree.name in node_group_users: 197 | users.append("Compositor") 198 | 199 | return distinct(users) 200 | 201 | 202 | def image_materials(image_key): 203 | # returns a list of material keys that use the image 204 | 205 | users = [] 206 | image = bpy.data.images[image_key] 207 | 208 | # list of node groups that use this image 209 | node_group_users = image_node_groups(image_key) 210 | 211 | for mat in bpy.data.materials: 212 | 213 | # if material uses a valid node tree, check each node 214 | if mat.use_nodes and mat.node_tree: 215 | for node in mat.node_tree.nodes: 216 | 217 | # if node is has a not none image attribute 218 | if hasattr(node, 'image') and node.image: 219 | 220 | # if the nodes image is our image 221 | if node.image.name == image.name: 222 | users.append(mat.name) 223 | 224 | # if image in node in node group in node tree 225 | elif node.type == 'GROUP': 226 | 227 | # if node group has a valid node tree and is in our 228 | # list of node groups that use this image 229 | if node.node_tree and \ 230 | node.node_tree.name in node_group_users: 231 | users.append(mat.name) 232 | 233 | return distinct(users) 234 | 235 | 236 | def image_node_groups(image_key): 237 | # returns a list of keys of node groups that use this image 238 | 239 | users = [] 240 | image = bpy.data.images[image_key] 241 | 242 | # for each node group 243 | for node_group in bpy.data.node_groups: 244 | 245 | # if node group contains our image 246 | if node_group_has_image(node_group.name, image.name): 247 | users.append(node_group.name) 248 | 249 | return distinct(users) 250 | 251 | 252 | def image_textures(image_key): 253 | # returns a list of texture keys that use the image 254 | 255 | users = [] 256 | image = bpy.data.images[image_key] 257 | 258 | # list of node groups that use this image 259 | node_group_users = image_node_groups(image_key) 260 | 261 | for texture in bpy.data.textures: 262 | 263 | # if texture uses a valid node tree, check each node 264 | if texture.use_nodes and texture.node_tree: 265 | for node in texture.node_tree.nodes: 266 | 267 | # check image nodes that use this image 268 | if hasattr(node, 'image') and node.image: 269 | if node.image.name == image.name: 270 | users.append(texture.name) 271 | 272 | # check for node groups that use this image 273 | elif hasattr(node, 'node_tree') and node.node_tree: 274 | 275 | # if node group is in our list of node groups that 276 | # use this image 277 | if node.node_tree.name in node_group_users: 278 | users.append(texture.name) 279 | 280 | # otherwise check the texture's image attribute 281 | else: 282 | 283 | # if texture uses an image 284 | if hasattr(texture, 'image') and texture.image: 285 | 286 | # if texture image is our image 287 | if texture.image.name == image.name: 288 | users.append(texture.name) 289 | 290 | return distinct(users) 291 | 292 | 293 | def image_worlds(image_key): 294 | # returns a list of world keys that use the image 295 | 296 | users = [] 297 | image = bpy.data.images[image_key] 298 | 299 | # list of node groups that use this image 300 | node_group_users = image_node_groups(image_key) 301 | 302 | for world in bpy.data.worlds: 303 | 304 | # if world uses a valid node tree, check each node 305 | if world.use_nodes and world.node_tree: 306 | for node in world.node_tree.nodes: 307 | 308 | # check image nodes 309 | if hasattr(node, 'image') and node.image: 310 | if node.image.name == image.name: 311 | users.append(world.name) 312 | 313 | # check for node groups that use this image 314 | elif hasattr(node, 'node_tree') and node.node_tree: 315 | if node.node_tree.name in node_group_users: 316 | users.append(world.name) 317 | 318 | return distinct(users) 319 | 320 | 321 | def light_all(light_key): 322 | # returns a list of keys of every data-block that uses this light 323 | 324 | return light_objects(light_key) 325 | 326 | 327 | def light_objects(light_key): 328 | # returns a list of light object keys that use the light data 329 | 330 | users = [] 331 | light = bpy.data.lights[light_key] 332 | 333 | for obj in bpy.data.objects: 334 | if obj.type == 'LIGHT' and obj.data: 335 | if obj.data.name == light.name: 336 | users.append(obj.name) 337 | 338 | return distinct(users) 339 | 340 | 341 | def material_all(material_key): 342 | # returns a list of keys of every data-block that uses this material 343 | 344 | return material_objects(material_key) 345 | 346 | 347 | def material_objects(material_key): 348 | # returns a list of object keys that use this material 349 | 350 | users = [] 351 | material = bpy.data.materials[material_key] 352 | 353 | for obj in bpy.data.objects: 354 | 355 | # if the object has the option to add materials 356 | if hasattr(obj, 'material_slots'): 357 | 358 | # for each material slot 359 | for slot in obj.material_slots: 360 | 361 | # if material slot has a valid material and it is our 362 | # material 363 | if slot.material and slot.material.name == material.name: 364 | users.append(obj.name) 365 | 366 | return distinct(users) 367 | 368 | 369 | def node_group_all(node_group_key): 370 | # returns a list of keys of every data-block that uses this node group 371 | 372 | return node_group_compositors(node_group_key) + \ 373 | node_group_materials(node_group_key) + \ 374 | node_group_node_groups(node_group_key) + \ 375 | node_group_textures(node_group_key) + \ 376 | node_group_worlds(node_group_key) 377 | 378 | 379 | def node_group_compositors(node_group_key): 380 | # returns a list containing "Compositor" if the node group is used in 381 | # the scene's compositor 382 | 383 | users = [] 384 | node_group = bpy.data.node_groups[node_group_key] 385 | 386 | # a list of node groups that use our node group 387 | node_group_users = node_group_node_groups(node_group_key) 388 | 389 | # if our compositor uses nodes and has a valid node tree 390 | if bpy.context.scene.use_nodes and bpy.context.scene.node_tree: 391 | 392 | # check each node in the compositor 393 | for node in bpy.context.scene.node_tree.nodes: 394 | 395 | # if the node is a group and has a valid node tree 396 | if hasattr(node, 'node_tree') and node.node_tree: 397 | 398 | # if the node group is our node group 399 | if node.node_tree.name == node_group.name: 400 | users.append("Compositor") 401 | 402 | # if the node group is in our list of node group users 403 | if node.node_tree.name in node_group_users: 404 | users.append("Compositor") 405 | 406 | return distinct(users) 407 | 408 | 409 | def node_group_materials(node_group_key): 410 | # returns a list of material keys that use the node group in their 411 | # node trees 412 | 413 | users = [] 414 | node_group = bpy.data.node_groups[node_group_key] 415 | 416 | # node groups that use this node group 417 | node_group_users = node_group_node_groups(node_group_key) 418 | 419 | for material in bpy.data.materials: 420 | 421 | # if material uses nodes and has a valid node tree, check each node 422 | if material.use_nodes and material.node_tree: 423 | for node in material.node_tree.nodes: 424 | 425 | # if node is a group node 426 | if hasattr(node, 'node_tree') and node.node_tree: 427 | 428 | # if node is the node group 429 | if node.node_tree.name == node_group.name: 430 | users.append(material.name) 431 | 432 | # if node is using a node group contains our node group 433 | if node.node_tree.name in node_group_users: 434 | users.append(material.name) 435 | 436 | return distinct(users) 437 | 438 | 439 | def node_group_node_groups(node_group_key): 440 | # returns a list of all node groups that use this node group in 441 | # their node tree 442 | 443 | users = [] 444 | node_group = bpy.data.node_groups[node_group_key] 445 | 446 | # for each search group 447 | for search_group in bpy.data.node_groups: 448 | 449 | # if the search group contains our node group 450 | if node_group_has_node_group( 451 | search_group.name, node_group.name): 452 | users.append(search_group.name) 453 | 454 | return distinct(users) 455 | 456 | 457 | def node_group_textures(node_group_key): 458 | # returns a list of texture keys that use this node group in their 459 | # node trees 460 | 461 | users = [] 462 | node_group = bpy.data.node_groups[node_group_key] 463 | 464 | # list of node groups that use this node group 465 | node_group_users = node_group_node_groups(node_group_key) 466 | 467 | for texture in bpy.data.textures: 468 | 469 | # if texture uses a valid node tree, check each node 470 | if texture.use_nodes and texture.node_tree: 471 | for node in texture.node_tree.nodes: 472 | 473 | # check if node is a node group and has a valid node tree 474 | if hasattr(node, 'node_tree') and node.node_tree: 475 | 476 | # if node is our node group 477 | if node.node_tree.name == node_group.name: 478 | users.append(texture.name) 479 | 480 | # if node is a node group that contains our node group 481 | if node.node_tree.name in node_group_users: 482 | users.append(texture.name) 483 | 484 | return distinct(users) 485 | 486 | 487 | def node_group_worlds(node_group_key): 488 | # returns a list of world keys that use the node group in their node 489 | # trees 490 | 491 | users = [] 492 | node_group = bpy.data.node_groups[node_group_key] 493 | 494 | # node groups that use this node group 495 | node_group_users = node_group_node_groups(node_group_key) 496 | 497 | for world in bpy.data.worlds: 498 | 499 | # if world uses nodes and has a valid node tree 500 | if world.use_nodes and world.node_tree: 501 | for node in world.node_tree.nodes: 502 | 503 | # if node is a node group and has a valid node tree 504 | if hasattr(node, 'node_tree') and node.node_tree: 505 | 506 | # if this node is our node group 507 | if node.node_tree.name == node_group.name: 508 | users.append(world.name) 509 | 510 | # if this node is one of the node groups that use 511 | # our node group 512 | elif node.node_tree.name in node_group_users: 513 | users.append(world.name) 514 | 515 | return distinct(users) 516 | 517 | 518 | def node_group_has_image(node_group_key, image_key): 519 | # recursively returns true if the node group contains this image 520 | # directly or if it contains a node group a node group that contains 521 | # the image indirectly 522 | 523 | has_image = False 524 | node_group = bpy.data.node_groups[node_group_key] 525 | image = bpy.data.images[image_key] 526 | 527 | # for each node in our search group 528 | for node in node_group.nodes: 529 | 530 | # base case 531 | # if node has a not none image attribute 532 | if hasattr(node, 'image') and node.image: 533 | 534 | # if the node group is our node group 535 | if node.image.name == image.name: 536 | has_image = True 537 | 538 | # recurse case 539 | # if node is a node group and has a valid node tree 540 | elif hasattr(node, 'node_tree') and node.node_tree: 541 | has_image = node_group_has_image( 542 | node.node_tree.name, image.name) 543 | 544 | # break the loop if the image is found 545 | if has_image: 546 | break 547 | 548 | return has_image 549 | 550 | 551 | def node_group_has_node_group(search_group_key, node_group_key): 552 | # returns true if a node group contains this node group 553 | 554 | has_node_group = False 555 | search_group = bpy.data.node_groups[search_group_key] 556 | node_group = bpy.data.node_groups[node_group_key] 557 | 558 | # for each node in our search group 559 | for node in search_group.nodes: 560 | 561 | # if node is a node group and has a valid node tree 562 | if hasattr(node, 'node_tree') and node.node_tree: 563 | 564 | if node.node_tree.name == "RG_MetallicMap": 565 | print(node.node_tree.name) 566 | print(node_group.name) 567 | 568 | # base case 569 | # if node group is our node group 570 | if node.node_tree.name == node_group.name: 571 | has_node_group = True 572 | 573 | # recurse case 574 | # if node group is any other node group 575 | else: 576 | has_node_group = node_group_has_node_group( 577 | node.node_tree.name, node_group.name) 578 | 579 | # break the loop if the node group is found 580 | if has_node_group: 581 | break 582 | 583 | return has_node_group 584 | 585 | 586 | def node_group_has_texture(node_group_key, texture_key): 587 | # returns true if a node group contains this image 588 | 589 | has_texture = False 590 | node_group = bpy.data.node_groups[node_group_key] 591 | texture = bpy.data.textures[texture_key] 592 | 593 | # for each node in our search group 594 | for node in node_group.nodes: 595 | 596 | # base case 597 | # if node has a not none image attribute 598 | if hasattr(node, 'texture') and node.texture: 599 | 600 | # if the node group is our node group 601 | if node.texture.name == texture.name: 602 | has_texture = True 603 | 604 | # recurse case 605 | # if node is a node group and has a valid node tree 606 | elif hasattr(node, 'node_tree') and node.node_tree: 607 | has_texture = node_group_has_texture( 608 | node.node_tree.name, texture.name) 609 | 610 | # break the loop if the texture is found 611 | if has_texture: 612 | break 613 | 614 | return has_texture 615 | 616 | 617 | def particle_all(particle_key): 618 | # returns a list of keys of every data-block that uses this particle 619 | # system 620 | 621 | return particle_objects(particle_key) 622 | 623 | 624 | def particle_objects(particle_key): 625 | # returns a list of object keys that use the particle system 626 | 627 | users = [] 628 | particle_system = bpy.data.particles[particle_key] 629 | 630 | for obj in bpy.data.objects: 631 | 632 | # if object can have a particle system 633 | if hasattr(obj, 'particle_systems'): 634 | for particle in obj.particle_systems: 635 | 636 | # if particle settings is our particle system 637 | if particle.settings.name == particle_system.name: 638 | users.append(obj.name) 639 | 640 | return distinct(users) 641 | 642 | 643 | def texture_all(texture_key): 644 | # returns a list of keys of every data-block that uses this texture 645 | 646 | return texture_brushes(texture_key) + \ 647 | texture_compositor(texture_key) + \ 648 | texture_objects(texture_key) + \ 649 | texture_node_groups(texture_key) + \ 650 | texture_particles(texture_key) 651 | 652 | 653 | def texture_brushes(texture_key): 654 | # returns a list of brush keys that use the texture 655 | 656 | users = [] 657 | texture = bpy.data.textures[texture_key] 658 | 659 | for brush in bpy.data.brushes: 660 | 661 | # if brush has a texture 662 | if brush.texture: 663 | 664 | # if brush texture is our texture 665 | if brush.texture.name == texture.name: 666 | users.append(brush.name) 667 | 668 | return distinct(users) 669 | 670 | 671 | def texture_compositor(texture_key): 672 | # returns a list containing "Compositor" if the texture is used in 673 | # the scene's compositor 674 | 675 | users = [] 676 | texture = bpy.data.textures[texture_key] 677 | 678 | # a list of node groups that use our image 679 | node_group_users = texture_node_groups(texture_key) 680 | 681 | # if our compositor uses nodes and has a valid node tree 682 | if bpy.context.scene.use_nodes and bpy.context.scene.node_tree: 683 | 684 | # check each node in the compositor 685 | for node in bpy.context.scene.node_tree.nodes: 686 | 687 | # if the node is an texture node with a valid texture 688 | if hasattr(node, 'texture') and node.texture: 689 | 690 | # if the node's texture is our texture 691 | if node.texture.name == texture.name: 692 | users.append("Compositor") 693 | 694 | # if the node is a group node with a valid node tree 695 | elif hasattr(node, 'node_tree') and node.node_tree: 696 | 697 | # if the node tree's name is in our list of node group 698 | # users 699 | if node.node_tree.name in node_group_users: 700 | users.append("Compositor") 701 | 702 | return distinct(users) 703 | 704 | 705 | def texture_objects(texture_key): 706 | # returns a list of object keys that use the texture in one of their 707 | # modifiers 708 | 709 | users = [] 710 | texture = bpy.data.textures[texture_key] 711 | 712 | # list of particle systems that use our texture 713 | particle_users = texture_particles(texture_key) 714 | 715 | # append objects that use the texture in a modifier 716 | for obj in bpy.data.objects: 717 | 718 | # if object can have modifiers applied to it 719 | if hasattr(obj, 'modifiers'): 720 | for modifier in obj.modifiers: 721 | 722 | # if the modifier has a texture attribute that is not None 723 | if hasattr(modifier, 'texture') \ 724 | and modifier.texture: 725 | if modifier.texture.name == texture.name: 726 | users.append(obj.name) 727 | 728 | # if the modifier has a mask_texture attribute that is 729 | # not None 730 | elif hasattr(modifier, 'mask_texture') \ 731 | and modifier.mask_texture: 732 | if modifier.mask_texture.name == texture.name: 733 | users.append(obj.name) 734 | 735 | # append objects that use the texture in a particle system 736 | for particle in particle_users: 737 | 738 | # append all objects that use the particle system 739 | users += particle_objects(particle) 740 | 741 | return distinct(users) 742 | 743 | 744 | def texture_node_groups(texture_key): 745 | # returns a list of keys of all node groups that use this texture 746 | 747 | users = [] 748 | texture = bpy.data.textures[texture_key] 749 | 750 | # for each node group 751 | for node_group in bpy.data.node_groups: 752 | 753 | # if node group contains our texture 754 | if node_group_has_texture( 755 | node_group.name, texture.name): 756 | users.append(node_group.name) 757 | 758 | return distinct(users) 759 | 760 | 761 | def texture_particles(texture_key): 762 | # returns a list of particle system keys that use the texture in 763 | # their texture slots 764 | 765 | users = [] 766 | texture = bpy.data.textures[texture_key] 767 | 768 | for particle in bpy.data.particles: 769 | 770 | # for each texture slot in the particle system 771 | for texture_slot in particle.texture_slots: 772 | 773 | # if texture slot has a texture that is not None 774 | if hasattr(texture_slot, 'texture') and texture_slot.texture: 775 | 776 | # if texture in texture slot is our texture 777 | if texture_slot.texture.name == texture.name: 778 | users.append(particle.name) 779 | 780 | return distinct(users) 781 | 782 | 783 | def distinct(seq): 784 | # returns a list of distinct elements 785 | 786 | return list(set(seq)) 787 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file handles the registration of the atomic_data_manager.ui package 22 | 23 | """ 24 | 25 | from atomic_data_manager.ui import main_panel_ui 26 | from atomic_data_manager.ui import stats_panel_ui 27 | from atomic_data_manager.ui import inspect_ui 28 | from atomic_data_manager.ui import missing_file_ui 29 | from atomic_data_manager.ui import missing_file_ui 30 | from atomic_data_manager.ui import pie_menu_ui 31 | from atomic_data_manager.ui import preferences_ui 32 | from atomic_data_manager.ui import support_me_ui 33 | 34 | 35 | def register(): 36 | # register preferences first so we can access variables in config.py 37 | preferences_ui.register() 38 | 39 | # register everything else 40 | main_panel_ui.register() 41 | stats_panel_ui.register() 42 | inspect_ui.register() 43 | missing_file_ui.register() 44 | pie_menu_ui.register() 45 | support_me_ui.register() 46 | 47 | 48 | def unregister(): 49 | main_panel_ui.unregister() 50 | stats_panel_ui.unregister() 51 | inspect_ui.unregister() 52 | missing_file_ui.unregister() 53 | pie_menu_ui.unregister() 54 | preferences_ui.unregister() 55 | support_me_ui.unregister() 56 | -------------------------------------------------------------------------------- /ui/inspect_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the inspection user interface. 22 | 23 | """ 24 | 25 | import bpy 26 | from bpy.utils import register_class 27 | from bpy.utils import unregister_class 28 | from atomic_data_manager.stats import users 29 | from atomic_data_manager.ui.utils import ui_layouts 30 | 31 | 32 | # bool that triggers an inspection update if it is True when the 33 | # inspection's draw() method is called 34 | inspection_update_trigger = False 35 | 36 | 37 | def update_inspection(self, context): 38 | global inspection_update_trigger 39 | inspection_update_trigger = True 40 | 41 | 42 | # Atomic Data Manager Inspect Collections UI Operator 43 | class ATOMIC_OT_inspect_collections(bpy.types.Operator): 44 | """Inspect Collections""" 45 | bl_idname = "atomic.inspect_collections" 46 | bl_label = "Inspect Collections" 47 | 48 | # user lists 49 | users_meshes = [] 50 | users_lights = [] 51 | users_cameras = [] 52 | users_others = [] 53 | users_children = [] 54 | 55 | def draw(self, context): 56 | global inspection_update_trigger 57 | atom = bpy.context.scene.atomic 58 | 59 | layout = self.layout 60 | 61 | # inspect collections box list 62 | ui_layouts.inspect_header( 63 | layout=layout, 64 | atom_prop="collections_field", 65 | data="collections" 66 | ) 67 | 68 | # inspection update code 69 | if inspection_update_trigger: 70 | 71 | # if key is valid, update the user lists 72 | if atom.collections_field in bpy.data.collections.keys(): 73 | self.users_meshes = \ 74 | users.collection_meshes(atom.collections_field) 75 | self.users_lights = \ 76 | users.collection_lights(atom.collections_field) 77 | self.users_cameras = \ 78 | users.collection_cameras(atom.collections_field) 79 | self.users_others = \ 80 | users.collection_others(atom.collections_field) 81 | self.users_children = \ 82 | users.collection_children(atom.collections_field) 83 | 84 | # if key is invalid, empty the user lists 85 | else: 86 | self.users_meshes = [] 87 | self.users_lights = [] 88 | self.users_cameras = [] 89 | self.users_others = [] 90 | self.users_children = [] 91 | 92 | inspection_update_trigger = False 93 | 94 | # mesh box list 95 | ui_layouts.box_list( 96 | layout=layout, 97 | title="Meshes", 98 | items=self.users_meshes, 99 | icon="OUTLINER_OB_MESH" 100 | ) 101 | 102 | # light box list 103 | ui_layouts.box_list( 104 | layout=layout, 105 | title="Lights", 106 | items=self.users_lights, 107 | icon="OUTLINER_OB_LIGHT" 108 | ) 109 | 110 | # camera box list 111 | ui_layouts.box_list( 112 | layout=layout, 113 | title="Cameras", 114 | items=self.users_cameras, 115 | icon="OUTLINER_OB_CAMERA" 116 | ) 117 | 118 | # other objects box list 119 | ui_layouts.box_list_diverse( 120 | layout=layout, 121 | title="Other", 122 | items=self.users_others 123 | ) 124 | 125 | # child collections box list 126 | ui_layouts.box_list( 127 | layout=layout, 128 | title="Child Collections", 129 | items=self.users_children, 130 | icon="OUTLINER_OB_GROUP_INSTANCE" 131 | ) 132 | 133 | row = layout.row() # extra row for spacing 134 | 135 | def execute(self, context): 136 | return {'FINISHED'} 137 | 138 | def invoke(self, context, event): 139 | # update inspection context 140 | atom = bpy.context.scene.atomic 141 | atom.active_inspection = "COLLECTIONS" 142 | 143 | # trigger update on invoke 144 | global inspection_update_trigger 145 | inspection_update_trigger = True 146 | 147 | # invoke inspect dialog 148 | wm = context.window_manager 149 | return wm.invoke_props_dialog(self) 150 | 151 | 152 | # Atomic Data Manager Inspect Images UI Operator 153 | class ATOMIC_OT_inspect_images(bpy.types.Operator): 154 | """Inspect Images""" 155 | bl_idname = "atomic.inspect_images" 156 | bl_label = "Inspect Images" 157 | 158 | # user lists 159 | users_compositors = [] 160 | users_materials = [] 161 | users_node_groups = [] 162 | users_textures = [] 163 | users_worlds = [] 164 | 165 | def draw(self, context): 166 | global inspection_update_trigger 167 | atom = bpy.context.scene.atomic 168 | 169 | layout = self.layout 170 | 171 | # inspect images header 172 | ui_layouts.inspect_header( 173 | layout=layout, 174 | atom_prop="images_field", 175 | data="images" 176 | ) 177 | 178 | # inspection update code 179 | if inspection_update_trigger: 180 | 181 | # if key is valid, update the user lists 182 | if atom.images_field in bpy.data.images.keys(): 183 | self.users_compositors = \ 184 | users.image_compositors(atom.images_field) 185 | self.users_materials = \ 186 | users.image_materials(atom.images_field) 187 | self.users_node_groups = \ 188 | users.image_node_groups(atom.images_field) 189 | self.users_textures = \ 190 | users.image_textures(atom.images_field) 191 | self.users_worlds = \ 192 | users.image_worlds(atom.images_field) 193 | 194 | # if key is invalid, empty the user lists 195 | else: 196 | self.users_compositors = [] 197 | self.users_materials = [] 198 | self.users_node_groups = [] 199 | self.users_textures = [] 200 | self.users_worlds = [] 201 | 202 | inspection_update_trigger = False 203 | 204 | # compositors box list 205 | ui_layouts.box_list( 206 | layout=layout, 207 | title="Compositors", 208 | items=self.users_compositors, 209 | icon="NODE_COMPOSITING" 210 | ) 211 | 212 | # materials box list 213 | ui_layouts.box_list( 214 | layout=layout, 215 | title="Materials", 216 | items=self.users_materials, 217 | icon="MATERIAL" 218 | ) 219 | 220 | # node groups box list 221 | ui_layouts.box_list( 222 | layout=layout, 223 | title="Node Groups", 224 | items=self.users_node_groups, 225 | icon="NODETREE" 226 | ) 227 | 228 | # textures box list 229 | ui_layouts.box_list( 230 | layout=layout, 231 | title="Textures", 232 | items=self.users_textures, 233 | icon="TEXTURE" 234 | ) 235 | 236 | # worlds box list 237 | ui_layouts.box_list( 238 | layout=layout, 239 | title="Worlds", 240 | items=self.users_worlds, 241 | icon="WORLD" 242 | ) 243 | 244 | row = layout.row() # extra row for spacing 245 | 246 | def execute(self, context): 247 | return {'FINISHED'} 248 | 249 | def invoke(self, context, event): 250 | # update inspection context 251 | atom = bpy.context.scene.atomic 252 | atom.active_inspection = "IMAGES" 253 | 254 | # trigger update on invoke 255 | global inspection_update_trigger 256 | inspection_update_trigger = True 257 | 258 | # invoke inspect dialog 259 | wm = context.window_manager 260 | return wm.invoke_props_dialog(self) 261 | 262 | # Atomic Data Manager Inspect Lights UI Operator 263 | class ATOMIC_OT_inspect_lights(bpy.types.Operator): 264 | """Inspect Lights""" 265 | bl_idname = "atomic.inspect_lights" 266 | bl_label = "Inspect Lights" 267 | 268 | # user lists 269 | users_objects = [] 270 | 271 | def draw(self, context): 272 | global inspection_update_trigger 273 | atom = bpy.context.scene.atomic 274 | 275 | layout = self.layout 276 | 277 | # inspect lights header 278 | ui_layouts.inspect_header( 279 | layout=layout, 280 | atom_prop="lights_field", 281 | data="lights" 282 | ) 283 | 284 | # inspection update code 285 | if inspection_update_trigger: 286 | # if key is valid, update the user lists 287 | if atom.lights_field in bpy.data.lights.keys(): 288 | self.users_objects = users.light_objects(atom.lights_field) 289 | 290 | # if key is invalid, empty the user lists 291 | else: 292 | self.users_objects = [] 293 | 294 | inspection_update_trigger = False 295 | 296 | # light objects box list 297 | ui_layouts.box_list( 298 | layout=layout, 299 | title="Light Objects", 300 | items=self.users_objects, 301 | icon="OUTLINER_OB_LIGHT" 302 | ) 303 | 304 | row = layout.row() # extra row for spacing 305 | 306 | def execute(self, context): 307 | return {'FINISHED'} 308 | 309 | def invoke(self, context, event): 310 | # update inspection context 311 | atom = bpy.context.scene.atomic 312 | atom.active_inspection = "LIGHTS" 313 | 314 | # trigger update on invoke 315 | global inspection_update_trigger 316 | inspection_update_trigger = True 317 | 318 | # invoke inspect dialog 319 | wm = context.window_manager 320 | return wm.invoke_props_dialog(self) 321 | 322 | 323 | # Atomic Data Manager Inspect Materials UI Operator 324 | class ATOMIC_OT_inspect_materials(bpy.types.Operator): 325 | """Inspect Materials""" 326 | bl_idname = "atomic.inspect_materials" 327 | bl_label = "Inspect Materials" 328 | 329 | # user lists 330 | users_objects = [] 331 | 332 | def draw(self, context): 333 | global inspection_update_trigger 334 | atom = bpy.context.scene.atomic 335 | 336 | layout = self.layout 337 | 338 | # inspect materials header 339 | ui_layouts.inspect_header( 340 | layout=layout, 341 | atom_prop="materials_field", 342 | data="materials" 343 | ) 344 | 345 | # inspection update code 346 | if inspection_update_trigger: 347 | 348 | # if key is valid, update the user lists 349 | if atom.materials_field in bpy.data.materials.keys(): 350 | self.users_objects = \ 351 | users.material_objects(atom.materials_field) 352 | 353 | # if key is invalid, empty the user lists 354 | else: 355 | self.users_objects = [] 356 | 357 | inspection_update_trigger = False 358 | 359 | # objects box list 360 | ui_layouts.box_list_diverse( 361 | layout=layout, 362 | title="Objects", 363 | items=self.users_objects 364 | ) 365 | 366 | row = layout.row() # extra row for spacing 367 | 368 | def execute(self, context): 369 | return {'FINISHED'} 370 | 371 | def invoke(self, context, event): 372 | # update inspection context 373 | atom = bpy.context.scene.atomic 374 | atom.active_inspection = "MATERIALS" 375 | 376 | # trigger update on invoke 377 | global inspection_update_trigger 378 | inspection_update_trigger = True 379 | 380 | # invoke inspect dialog 381 | wm = context.window_manager 382 | return wm.invoke_props_dialog(self) 383 | 384 | 385 | # Atomic Data Manager Inspect Node Groups UI Operator 386 | class ATOMIC_OT_inspect_node_groups(bpy.types.Operator): 387 | """Inspect Node Groups""" 388 | bl_idname = "atomic.inspect_node_groups" 389 | bl_label = "Inspect Node Groups" 390 | 391 | # user lists 392 | users_compositors = [] 393 | users_materials = [] 394 | users_node_groups = [] 395 | users_textures = [] 396 | users_worlds = [] 397 | 398 | def draw(self, context): 399 | global inspection_update_trigger 400 | atom = bpy.context.scene.atomic 401 | 402 | layout = self.layout 403 | 404 | # inspect node groups header 405 | ui_layouts.inspect_header( 406 | layout=layout, 407 | atom_prop="node_groups_field", 408 | data="node_groups" 409 | ) 410 | 411 | # inspection update code 412 | if inspection_update_trigger: 413 | 414 | # if key is valid, update the user lists 415 | if atom.node_groups_field in bpy.data.node_groups.keys(): 416 | 417 | self.users_compositors = \ 418 | users.node_group_compositors(atom.node_groups_field) 419 | self.users_materials = \ 420 | users.node_group_materials(atom.node_groups_field) 421 | self.users_node_groups = \ 422 | users.node_group_node_groups(atom.node_groups_field) 423 | self.users_textures = \ 424 | users.node_group_textures(atom.node_groups_field) 425 | self.users_worlds = \ 426 | users.node_group_worlds(atom.node_groups_field) 427 | 428 | # if key is invalid, empty the user lists 429 | else: 430 | self.users_compositors = [] 431 | self.users_materials = [] 432 | self.users_node_groups = [] 433 | self.users_textures = [] 434 | self.users_worlds = [] 435 | 436 | inspection_update_trigger = False 437 | 438 | # compositors box list 439 | ui_layouts.box_list( 440 | layout=layout, 441 | title="Compositors", 442 | items=self.users_compositors, 443 | icon="NODE_COMPOSITING" 444 | ) 445 | 446 | # materials box list 447 | ui_layouts.box_list( 448 | layout=layout, 449 | title="Materials", 450 | items=self.users_materials, 451 | icon="MATERIAL" 452 | ) 453 | 454 | # node groups box list 455 | ui_layouts.box_list( 456 | layout=layout, 457 | title="Node Groups", 458 | items=self.users_node_groups, 459 | icon="NODETREE" 460 | ) 461 | 462 | # textures box list 463 | ui_layouts.box_list( 464 | layout=layout, 465 | title="Textures", 466 | items=self.users_textures, 467 | icon="TEXTURE" 468 | ) 469 | 470 | # world box list 471 | ui_layouts.box_list( 472 | layout=layout, 473 | title="Worlds", 474 | items=self.users_worlds, 475 | icon="WORLD" 476 | ) 477 | 478 | row = layout.row() # extra row for spacing 479 | 480 | def execute(self, context): 481 | return {'FINISHED'} 482 | 483 | def invoke(self, context, event): 484 | # update inspection context 485 | atom = bpy.context.scene.atomic 486 | atom.active_inspection = "NODE_GROUPS" 487 | 488 | # trigger update on invoke 489 | global inspection_update_trigger 490 | inspection_update_trigger = True 491 | 492 | # invoke inspect dialog 493 | wm = context.window_manager 494 | return wm.invoke_props_dialog(self) 495 | 496 | 497 | # Atomic Data Manager Inspect Particles UI Operator 498 | class ATOMIC_OT_inspect_particles(bpy.types.Operator): 499 | """Inspect Particle Systems""" 500 | bl_idname = "atomic.inspect_particles" 501 | bl_label = "Inspect Particles" 502 | 503 | # user lists 504 | users_objects = [] 505 | 506 | def draw(self, context): 507 | global inspection_update_trigger 508 | atom = bpy.context.scene.atomic 509 | 510 | layout = self.layout 511 | 512 | # inspect particles header 513 | ui_layouts.inspect_header( 514 | layout=layout, 515 | atom_prop="particles_field", 516 | data="particles" 517 | ) 518 | 519 | # inspection update code 520 | if inspection_update_trigger: 521 | 522 | # if key is valid, update the user lists 523 | if atom.particles_field in bpy.data.particles.keys(): 524 | 525 | self.users_objects = \ 526 | users.particle_objects(atom.particles_field) 527 | 528 | # if key is invalid, empty the user lists 529 | else: 530 | self.users_objects = [] 531 | 532 | inspection_update_trigger = False 533 | 534 | # objects box list 535 | ui_layouts.box_list( 536 | layout=layout, 537 | title="Objects", 538 | items=self.users_objects, 539 | icon="OUTLINER_OB_MESH" 540 | ) 541 | 542 | row = layout.row() # extra row for spacing 543 | 544 | def execute(self, context): 545 | return {'FINISHED'} 546 | 547 | def invoke(self, context, event): 548 | # update inspection context 549 | atom = bpy.context.scene.atomic 550 | atom.active_inspection = "PARTICLES" 551 | 552 | # trigger update on invoke 553 | global inspection_update_trigger 554 | inspection_update_trigger = True 555 | 556 | # invoke inspect dialog 557 | wm = context.window_manager 558 | return wm.invoke_props_dialog(self) 559 | 560 | 561 | # Atomic Data Manager Inspect Textures UI Operator 562 | class ATOMIC_OT_inspect_textures(bpy.types.Operator): 563 | """Inspect Textures""" 564 | bl_idname = "atomic.inspect_textures" 565 | bl_label = "Inspect Textures" 566 | 567 | # user lists 568 | users_compositors = [] 569 | users_brushes = [] 570 | users_particles = [] 571 | users_objects = [] 572 | 573 | def draw(self, context): 574 | global inspection_update_trigger 575 | atom = bpy.context.scene.atomic 576 | 577 | layout = self.layout 578 | 579 | # inspect textures header 580 | ui_layouts.inspect_header( 581 | layout=layout, 582 | atom_prop="textures_field", 583 | data="textures" 584 | ) 585 | 586 | # inspection update code 587 | if inspection_update_trigger: 588 | 589 | # if the key is valid, update the user lists 590 | if atom.textures_field in bpy.data.textures.keys(): 591 | 592 | self.users_compositors = \ 593 | users.texture_compositor(atom.textures_field) 594 | self.users_brushes = \ 595 | users.texture_brushes(atom.textures_field) 596 | self.users_objects = \ 597 | users.texture_objects(atom.textures_field) 598 | self.users_particles = \ 599 | users.texture_particles(atom.textures_field) 600 | 601 | # if the key is invalid, set empty the user lists 602 | else: 603 | self.users_compositors = [] 604 | self.users_brushes = [] 605 | self.users_particles = [] 606 | self.users_objects = [] 607 | 608 | inspection_update_trigger = False 609 | 610 | # brushes box list 611 | ui_layouts.box_list( 612 | layout=layout, 613 | title="Brushes", 614 | items=self.users_brushes, 615 | icon="BRUSH_DATA" 616 | ) 617 | 618 | # compositors box list 619 | ui_layouts.box_list( 620 | layout=layout, 621 | title="Compositors", 622 | items=self.users_compositors, 623 | icon="NODE_COMPOSITING" 624 | ) 625 | 626 | # particles box list 627 | ui_layouts.box_list( 628 | layout=layout, 629 | title="Particles", 630 | items=self.users_particles, 631 | icon="PARTICLES" 632 | ) 633 | 634 | # objects box list 635 | ui_layouts.box_list_diverse( 636 | layout=layout, 637 | title="Objects", 638 | items=self.users_objects, 639 | ) 640 | 641 | row = layout.row() # extra row for spacing 642 | 643 | def execute(self, context): 644 | return {'FINISHED'} 645 | 646 | def invoke(self, context, event): 647 | # update inspection context 648 | atom = bpy.context.scene.atomic 649 | atom.active_inspection = "TEXTURES" 650 | 651 | # trigger update on invoke 652 | global inspection_update_trigger 653 | inspection_update_trigger = True 654 | 655 | # invoke inspect dialog 656 | wm = context.window_manager 657 | return wm.invoke_props_dialog(self) 658 | 659 | 660 | # Atomic Data Manager Inspect Worlds UI Operator 661 | class ATOMIC_OT_inspect_worlds(bpy.types.Operator): 662 | """Inspect Worlds""" 663 | bl_idname = "atomic.inspect_worlds" 664 | bl_label = "Inspect Worlds" 665 | 666 | def draw(self, context): 667 | layout = self.layout 668 | 669 | # inspect worlds header 670 | ui_layouts.inspect_header( 671 | layout=layout, 672 | atom_prop="worlds_field", 673 | data="worlds" 674 | ) 675 | 676 | # worlds box list 677 | ui_layouts.box_list( 678 | layout=layout, 679 | title="Worlds in Scene", 680 | items=bpy.data.worlds.keys(), 681 | icon="WORLD" 682 | ) 683 | 684 | row = layout.row() # extra row for spacing 685 | 686 | def execute(self, context): 687 | return {'FINISHED'} 688 | 689 | def invoke(self, context, event): 690 | # update inspection context 691 | atom = bpy.context.scene.atomic 692 | atom.active_inspection = "WORLDS" 693 | 694 | # trigger update on invoke 695 | global inspection_update_trigger 696 | inspection_update_trigger = True 697 | 698 | # invoke inspect dialog 699 | wm = context.window_manager 700 | return wm.invoke_props_dialog(self) 701 | 702 | 703 | reg_list = [ 704 | ATOMIC_OT_inspect_collections, 705 | ATOMIC_OT_inspect_images, 706 | ATOMIC_OT_inspect_lights, 707 | ATOMIC_OT_inspect_materials, 708 | ATOMIC_OT_inspect_node_groups, 709 | ATOMIC_OT_inspect_particles, 710 | ATOMIC_OT_inspect_textures, 711 | ATOMIC_OT_inspect_worlds 712 | ] 713 | 714 | 715 | def register(): 716 | for cls in reg_list: 717 | register_class(cls) 718 | 719 | 720 | def unregister(): 721 | for cls in reg_list: 722 | unregister_class(cls) 723 | -------------------------------------------------------------------------------- /ui/main_panel_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the primary Atomic Data Manager panel that will 22 | appear in the Scene tab of the Properties panel. 23 | 24 | This panel contains the Nuke/Clean/Undo buttons as well as the data 25 | category toggles and the category selection tools. 26 | 27 | """ 28 | 29 | import bpy 30 | from bpy.utils import register_class 31 | from bpy.utils import unregister_class 32 | from atomic_data_manager.stats import count 33 | from atomic_data_manager.ui.utils import ui_layouts 34 | 35 | 36 | # Atomic Data Manager Main Panel 37 | class ATOMIC_PT_main_panel(bpy.types.Panel): 38 | """The main Atomic Data Manager panel""" 39 | bl_label = "Atomic Data Manager" 40 | bl_space_type = "PROPERTIES" 41 | bl_region_type = "WINDOW" 42 | bl_context = "scene" 43 | 44 | def draw(self, context): 45 | layout = self.layout 46 | atom = bpy.context.scene.atomic 47 | category_props = [ 48 | atom.collections, 49 | atom.images, 50 | atom.lights, 51 | atom.materials, 52 | atom.node_groups, 53 | atom.particles, 54 | atom.textures, 55 | atom.worlds 56 | ] 57 | 58 | # nuke and clean buttons 59 | row = layout.row(align=True) 60 | row.scale_y = 2.0 61 | row.operator("atomic.nuke", text="Nuke", icon="GHOST_ENABLED") 62 | row.operator("atomic.clean", text="Clean", icon="PARTICLEMODE") 63 | row.operator("atomic.undo", text="Undo", icon="LOOP_BACK") 64 | 65 | row = layout.row() 66 | 67 | # category toggles 68 | split = layout.split(align=False) 69 | 70 | # left column 71 | col = split.column(align=True) 72 | 73 | # collections buttons 74 | splitcol = col.split(factor=0.8, align=True) 75 | 76 | splitcol.prop( 77 | atom, 78 | "collections", 79 | text="Collections", 80 | icon='GROUP', 81 | toggle=True 82 | ) 83 | 84 | splitcol.operator( 85 | "atomic.inspect_collections", 86 | icon='VIEWZOOM', 87 | text="" 88 | ) 89 | 90 | # lights buttons 91 | splitcol = col.split(factor=0.8, align=True) 92 | 93 | splitcol.prop( 94 | atom, 95 | "lights", 96 | text="Lights", 97 | icon='LIGHT', 98 | toggle=True 99 | ) 100 | 101 | splitcol.operator( 102 | "atomic.inspect_lights", 103 | icon='VIEWZOOM', 104 | text="" 105 | ) 106 | 107 | # node groups buttons 108 | splitcol = col.split(factor=0.8, align=True) 109 | 110 | splitcol.prop( 111 | atom, 112 | "node_groups", 113 | text="Node Groups", 114 | icon='NODETREE', 115 | toggle=True 116 | ) 117 | 118 | splitcol.operator( 119 | "atomic.inspect_node_groups", 120 | icon='VIEWZOOM', 121 | text="" 122 | ) 123 | 124 | # textures button 125 | splitcol = col.split(factor=0.8, align=True) 126 | 127 | splitcol.prop( 128 | atom, 129 | "textures", 130 | text="Textures", 131 | icon='TEXTURE', 132 | toggle=True 133 | ) 134 | 135 | splitcol.operator( 136 | "atomic.inspect_textures", 137 | icon='VIEWZOOM', 138 | text="" 139 | ) 140 | 141 | # right column 142 | col = split.column(align=True) 143 | 144 | # images buttons 145 | splitcol = col.split(factor=0.8, align=True) 146 | 147 | splitcol.prop( 148 | atom, 149 | "images", 150 | text="Images", 151 | toggle=True, 152 | icon='IMAGE_DATA' 153 | ) 154 | 155 | splitcol.operator( 156 | "atomic.inspect_images", 157 | icon='VIEWZOOM', 158 | text="" 159 | ) 160 | 161 | # materials buttons 162 | splitcol = col.split(factor=0.8, align=True) 163 | 164 | splitcol.prop( 165 | atom, 166 | "materials", 167 | text="Materials", 168 | icon='MATERIAL', 169 | toggle=True 170 | ) 171 | 172 | splitcol.operator( 173 | "atomic.inspect_materials", 174 | icon='VIEWZOOM', 175 | text="" 176 | ) 177 | 178 | # particles buttons 179 | splitcol = col.split(factor=0.8, align=True) 180 | 181 | splitcol.prop( 182 | atom, 183 | "particles", 184 | text="Particles", 185 | icon='PARTICLES', 186 | toggle=True 187 | ) 188 | 189 | splitcol.operator( 190 | "atomic.inspect_particles", 191 | icon='VIEWZOOM', 192 | text="" 193 | ) 194 | 195 | # worlds buttons 196 | splitcol = col.split(factor=0.8, align=True) 197 | splitcol.prop( 198 | atom, 199 | "worlds", 200 | text="Worlds", 201 | icon='WORLD', 202 | toggle=True 203 | ) 204 | 205 | splitcol.operator( 206 | "atomic.inspect_worlds", 207 | icon='VIEWZOOM', 208 | text="" 209 | ) 210 | 211 | # selection operators 212 | row = layout.row(align=True) 213 | 214 | row.operator( 215 | "atomic.smart_select", 216 | text='Smart Select', 217 | icon='ZOOM_SELECTED' 218 | ) 219 | 220 | if all(prop is True for prop in category_props): 221 | row.operator( 222 | "atomic.deselect_all", 223 | text="Deselect All", 224 | icon='RESTRICT_SELECT_ON' 225 | ) 226 | 227 | else: 228 | row.operator( 229 | "atomic.select_all", 230 | text="Select All", 231 | icon='RESTRICT_SELECT_OFF' 232 | ) 233 | 234 | 235 | reg_list = [ATOMIC_PT_main_panel] 236 | 237 | 238 | def register(): 239 | for cls in reg_list: 240 | register_class(cls) 241 | 242 | 243 | def unregister(): 244 | for cls in reg_list: 245 | unregister_class(cls) 246 | -------------------------------------------------------------------------------- /ui/missing_file_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the user interface for the missing file dialog that 22 | pops up when missing files are detected on file load. 23 | 24 | """ 25 | 26 | import bpy 27 | from bpy.utils import register_class 28 | from bpy.utils import unregister_class 29 | from bpy.app.handlers import persistent 30 | from atomic_data_manager import config 31 | from atomic_data_manager.stats import missing 32 | from atomic_data_manager.ui.utils import ui_layouts 33 | 34 | 35 | # Atomic Data Manager Detect Missing Files Popup 36 | class ATOMIC_OT_detect_missing(bpy.types.Operator): 37 | """Detect missing files in this project""" 38 | bl_idname = "atomic.detect_missing" 39 | bl_label = "Missing File Detection" 40 | 41 | # missing file lists 42 | missing_images = [] 43 | missing_libraries = [] 44 | 45 | # missing file recovery option enum property 46 | recovery_option: bpy.props.EnumProperty( 47 | items=[ 48 | ( 49 | 'IGNORE', 50 | 'Ignore Missing Files', 51 | 'Ignore the missing files and leave them offline' 52 | ), 53 | ( 54 | 'RELOAD', 55 | 'Reload Missing Files', 56 | 'Reload the missing files from their existing file paths' 57 | ), 58 | ( 59 | 'REMOVE', 60 | 'Remove Missing Files', 61 | 'Remove the missing files from the project' 62 | ), 63 | ( 64 | 'SEARCH', 65 | 'Search for Missing Files (under development)', 66 | 'Search for the missing files in a directory' 67 | ), 68 | ( 69 | 'REPLACE', 70 | 'Specify Replacement Files (under development)', 71 | 'Replace missing files with new files' 72 | ), 73 | ], 74 | default='IGNORE' 75 | ) 76 | 77 | def draw(self, context): 78 | layout = self.layout 79 | 80 | # missing files interface if missing files are found 81 | if self.missing_images or self.missing_libraries: 82 | 83 | # header warning 84 | row = layout.row() 85 | row.label( 86 | text="Atomic has detected one or more missing files in " 87 | "your project!" 88 | ) 89 | 90 | # missing images box list 91 | if self.missing_images: 92 | ui_layouts.box_list( 93 | layout=layout, 94 | title="Images", 95 | items=self.missing_images, 96 | icon="IMAGE_DATA", 97 | columns=3 98 | ) 99 | 100 | # missing libraries box list 101 | if self.missing_libraries: 102 | ui_layouts.box_list( 103 | layout=layout, 104 | title="Libraries", 105 | items=self.missing_libraries, 106 | icon="LIBRARY_DATA_DIRECT", 107 | columns=3 108 | ) 109 | 110 | row = layout.separator() # extra space 111 | 112 | # recovery option selection 113 | row = layout.row() 114 | row.label(text="What would you like to do?") 115 | 116 | row = layout.row() 117 | row.prop(self, 'recovery_option', text="") 118 | 119 | # missing files interface if no missing files are found 120 | else: 121 | row = layout.row() 122 | row.label(text="No missing files were found!") 123 | 124 | # empty box list 125 | ui_layouts.box_list( 126 | layout=layout 127 | ) 128 | 129 | row = layout.separator() # extra space 130 | 131 | def execute(self, context): 132 | 133 | # ignore missing files will take no action 134 | 135 | # reload missing files 136 | if self.recovery_option == 'RELOAD': 137 | bpy.ops.atomic.reload_missing('INVOKE_DEFAULT') 138 | 139 | # remove missing files 140 | elif self.recovery_option == 'REMOVE': 141 | bpy.ops.atomic.remove_missing('INVOKE_DEFAULT') 142 | 143 | # search for missing files 144 | elif self.recovery_option == 'SEARCH': 145 | bpy.ops.atomic.search_missing('INVOKE_DEFAULT') 146 | 147 | # replace missing files 148 | elif self.recovery_option == 'REPLACE': 149 | bpy.ops.atomic.replace_missing('INVOKE_DEFAULT') 150 | 151 | return {'FINISHED'} 152 | 153 | def invoke(self, context, event): 154 | 155 | # update missing file lists 156 | self.missing_images = missing.images() 157 | self.missing_libraries = missing.libraries() 158 | 159 | wm = context.window_manager 160 | 161 | # invoke large dialog if there are missing files 162 | if self.missing_images or self.missing_libraries: 163 | return wm.invoke_props_dialog(self, width=500) 164 | 165 | # invoke small dialog if there are no missing files 166 | else: 167 | return wm.invoke_popup(self, width=300) 168 | 169 | 170 | @persistent 171 | def autodetect_missing_files(dummy=None): 172 | # invokes the detect missing popup when missing files are detected upon 173 | # loading a new Blender project 174 | if config.enable_missing_file_warning and \ 175 | (missing.images() or missing.libraries()): 176 | bpy.ops.atomic.detect_missing('INVOKE_DEFAULT') 177 | 178 | 179 | reg_list = [ATOMIC_OT_detect_missing] 180 | 181 | 182 | def register(): 183 | for item in reg_list: 184 | register_class(item) 185 | 186 | # run missing file auto-detection after loading a Blender file 187 | bpy.app.handlers.load_post.append(autodetect_missing_files) 188 | 189 | 190 | def unregister(): 191 | for item in reg_list: 192 | unregister_class(item) 193 | 194 | # stop running missing file auto-detection after loading a Blender file 195 | bpy.app.handlers.load_post.remove(autodetect_missing_files) 196 | -------------------------------------------------------------------------------- /ui/pie_menu_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains Atomic's pie menu UI and its pie menu keymap 22 | registration. 23 | 24 | """ 25 | 26 | import bpy 27 | from bpy.utils import register_class 28 | from bpy.utils import unregister_class 29 | 30 | 31 | # Atomic Data Manager Main Pie Menu 32 | class ATOMIC_MT_main_pie(bpy.types.Menu): 33 | bl_idname = "ATOMIC_MT_main_pie" 34 | bl_label = "Atomic Data Manager" 35 | 36 | def draw(self, context): 37 | layout = self.layout 38 | pie = layout.menu_pie() 39 | 40 | # nuke all operator 41 | pie.operator( 42 | "atomic.nuke_all", 43 | text="Nuke All", 44 | icon="GHOST_ENABLED" 45 | ) 46 | 47 | # clean all operator 48 | pie.operator( 49 | "atomic.clean_all", 50 | text="Clean All", 51 | icon="PARTICLEMODE" 52 | ) 53 | 54 | # undo operator 55 | pie.operator( 56 | "atomic.detect_missing", 57 | text="Detect Missing Files", 58 | icon="SHADERFX" 59 | ) 60 | 61 | # inspect category operator 62 | pie.operator( 63 | "wm.call_menu_pie", 64 | text="Inspect", 65 | icon="VIEWZOOM" 66 | ).name = "ATOMIC_MT_inspect_pie" 67 | 68 | # nuke category operator 69 | pie.operator( 70 | "wm.call_menu_pie", 71 | text="Nuke", 72 | icon="GHOST_ENABLED" 73 | ).name = "ATOMIC_MT_nuke_pie" 74 | 75 | # clean category operator 76 | pie.operator( 77 | "wm.call_menu_pie", 78 | text="Clean", 79 | icon="PARTICLEMODE" 80 | ).name = "ATOMIC_MT_clean_pie" 81 | 82 | 83 | # Atomic Data Manager Nuke Pie Menu 84 | class ATOMIC_MT_nuke_pie(bpy.types.Menu): 85 | bl_idname = "ATOMIC_MT_nuke_pie" 86 | bl_label = "Atomic Nuke" 87 | 88 | def draw(self, context): 89 | layout = self.layout 90 | pie = layout.menu_pie() 91 | 92 | # nuke node groups operator 93 | pie.operator("atomic.nuke_node_groups", icon="NODETREE") 94 | 95 | # nuke materials operator 96 | pie.operator("atomic.nuke_materials", icon="MATERIAL") 97 | 98 | # nuke worlds operator 99 | pie.operator("atomic.nuke_worlds", icon="WORLD") 100 | 101 | # nuke collections operator 102 | pie.operator("atomic.nuke_collections", icon="GROUP") 103 | 104 | # nuke lights operator 105 | pie.operator("atomic.nuke_lights", icon="LIGHT") 106 | 107 | # nuke images operator 108 | pie.operator("atomic.nuke_images", icon="IMAGE_DATA") 109 | 110 | # nuke textures operator 111 | pie.operator("atomic.nuke_textures", icon="TEXTURE") 112 | 113 | # nuke particles operator 114 | pie.operator("atomic.nuke_particles", icon="PARTICLES") 115 | 116 | 117 | # Atomic Data Manager Clean Pie Menu 118 | class ATOMIC_MT_clean_pie(bpy.types.Menu): 119 | bl_idname = "ATOMIC_MT_clean_pie" 120 | bl_label = "Atomic Clean" 121 | 122 | def draw(self, context): 123 | layout = self.layout 124 | pie = layout.menu_pie() 125 | 126 | # clean node groups operator 127 | pie.operator("atomic.clean_node_groups", icon="NODETREE") 128 | 129 | # clean materials operator 130 | pie.operator("atomic.clean_materials", icon="MATERIAL") 131 | 132 | # clean worlds operator 133 | pie.operator("atomic.clean_worlds", icon="WORLD") 134 | 135 | # clean collections operator 136 | pie.operator("atomic.clean_collections", icon="GROUP") 137 | 138 | # clean lights operator 139 | pie.operator("atomic.clean_lights", icon="LIGHT") 140 | 141 | # clean images operator 142 | pie.operator("atomic.clean_images", icon="IMAGE_DATA") 143 | 144 | # clean textures operator 145 | pie.operator("atomic.clean_textures", icon="TEXTURE") 146 | 147 | # clean materials operator 148 | pie.operator("atomic.clean_particles", icon="PARTICLES") 149 | 150 | 151 | # Atomic Data Manager Inspect Pie Menu 152 | class ATOMIC_MT_inspect_pie(bpy.types.Menu): 153 | bl_idname = "ATOMIC_MT_inspect_pie" 154 | bl_label = "Atomic Inspect" 155 | 156 | def draw(self, context): 157 | layout = self.layout 158 | pie = layout.menu_pie() 159 | 160 | # inspect node groups operator 161 | pie.operator("atomic.inspect_node_groups", icon="NODETREE") 162 | 163 | # inspect materials operator 164 | pie.operator("atomic.inspect_materials", icon="MATERIAL") 165 | 166 | # inspect worlds operator 167 | pie.operator("atomic.inspect_worlds", icon="WORLD") 168 | 169 | # inspect groups operator 170 | pie.operator("atomic.inspect_collections", icon="GROUP") 171 | 172 | # inspect lights operator 173 | pie.operator("atomic.inspect_lights", icon="LIGHT") 174 | 175 | # inspect images operator 176 | pie.operator("atomic.inspect_images", icon="IMAGE_DATA") 177 | 178 | # inspect textures operator 179 | pie.operator("atomic.inspect_textures", icon="TEXTURE") 180 | 181 | # inspect particles operator 182 | pie.operator("atomic.inspect_particles", icon="PARTICLES") 183 | 184 | 185 | reg_list = [ 186 | ATOMIC_MT_main_pie, 187 | ATOMIC_MT_nuke_pie, 188 | ATOMIC_MT_clean_pie, 189 | ATOMIC_MT_inspect_pie 190 | ] 191 | 192 | 193 | def register(): 194 | for cls in reg_list: 195 | register_class(cls) 196 | 197 | 198 | def unregister(): 199 | for cls in reg_list: 200 | unregister_class(cls) 201 | -------------------------------------------------------------------------------- /ui/preferences_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the Atomic preferences UI, preferences properties, and 22 | some functions for syncing the preference properties with external factors. 23 | 24 | """ 25 | 26 | import bpy 27 | from bpy.utils import register_class 28 | from bpy.utils import unregister_class 29 | from atomic_data_manager import config 30 | from atomic_data_manager.updater import addon_updater_ops 31 | 32 | 33 | def set_enable_support_me_popup(value): 34 | # sets the value of the enable_support_me_popup boolean property 35 | 36 | bpy.context.preferences.addons["atomic_data_manager"]\ 37 | .preferences.enable_support_me_popup = value 38 | copy_prefs_to_config(None, None) 39 | bpy.ops.wm.save_userpref() 40 | 41 | 42 | def set_last_popup_day(day): 43 | # sets the value of the last_popup_day float property 44 | 45 | bpy.context.preferences.addons["atomic_data_manager"]\ 46 | .preferences.last_popup_day = day 47 | copy_prefs_to_config(None, None) 48 | 49 | 50 | def copy_prefs_to_config(self, context): 51 | # copies the values of Atomic's preferences to the variables in 52 | # config.py for global use 53 | 54 | preferences = bpy.context.preferences 55 | 56 | atomic_preferences = preferences.addons['atomic_data_manager']\ 57 | .preferences 58 | 59 | # visible atomic preferences 60 | config.enable_missing_file_warning = \ 61 | atomic_preferences.enable_missing_file_warning 62 | 63 | config.enable_pie_menu_ui = \ 64 | atomic_preferences.enable_pie_menu_ui 65 | 66 | config.enable_support_me_popup = \ 67 | atomic_preferences.enable_support_me_popup 68 | 69 | config.include_fake_users = \ 70 | atomic_preferences.include_fake_users 71 | 72 | # hidden atomic preferences 73 | config.pie_menu_type = \ 74 | atomic_preferences.pie_menu_type 75 | 76 | config.pie_menu_alt = \ 77 | atomic_preferences.pie_menu_alt 78 | 79 | config.pie_menu_any = \ 80 | atomic_preferences.pie_menu_any 81 | 82 | config.pie_menu_ctrl = \ 83 | atomic_preferences.pie_menu_ctrl 84 | 85 | config.pie_menu_oskey = \ 86 | atomic_preferences.pie_menu_oskey 87 | 88 | config.pie_menu_shift = \ 89 | atomic_preferences.pie_menu_shift 90 | 91 | config.last_popup_day = \ 92 | atomic_preferences.last_popup_day 93 | 94 | 95 | def update_pie_menu_hotkeys(self, context): 96 | preferences = bpy.context.preferences 97 | atomic_preferences = preferences.addons['atomic_data_manager'] \ 98 | .preferences 99 | 100 | # add the hotkeys if the preference is enabled 101 | if atomic_preferences.enable_pie_menu_ui: 102 | add_pie_menu_hotkeys() 103 | 104 | # remove the hotkeys otherwise 105 | else: 106 | remove_pie_menu_hotkeys() 107 | 108 | 109 | def add_pie_menu_hotkeys(): 110 | # adds the pie menu hotkeys to blender's addon keymaps 111 | 112 | global keymaps 113 | keyconfigs = bpy.context.window_manager.keyconfigs.addon 114 | 115 | # check to see if a window keymap already exists 116 | if "Window" in keyconfigs.keymaps.keys(): 117 | km = keyconfigs.keymaps['Window'] 118 | 119 | # if not, crate a new one 120 | else: 121 | km = keyconfigs.keymaps.new( 122 | name="Window", 123 | space_type='EMPTY', 124 | region_type='WINDOW' 125 | ) 126 | 127 | # add a new keymap item to that keymap 128 | kmi = km.keymap_items.new( 129 | idname="atomic.invoke_pie_menu_ui", 130 | type=config.pie_menu_type, 131 | value="PRESS", 132 | alt=config.pie_menu_alt, 133 | any=config.pie_menu_any, 134 | ctrl=config.pie_menu_ctrl, 135 | oskey=config.pie_menu_oskey, 136 | shift=config.pie_menu_shift, 137 | ) 138 | 139 | # # point the keymap item to our pie menu 140 | # kmi.properties.name = "ATOMIC_MT_main_pie" 141 | keymaps.append((km, kmi)) 142 | 143 | 144 | def remove_pie_menu_hotkeys(): 145 | # removes the pie menu hotkeys from blender's addon keymaps if they 146 | # exist there 147 | 148 | global keymaps 149 | 150 | # remove each hotkey in our keymaps list if it exists in blenders 151 | # addon keymaps 152 | for km, kmi in keymaps: 153 | km.keymap_items.remove(kmi) 154 | 155 | # clear our keymaps list 156 | keymaps.clear() 157 | 158 | 159 | # Atomic Data Manager Preference Panel UI 160 | class ATOMIC_PT_preferences_panel(bpy.types.AddonPreferences): 161 | bl_idname = "atomic_data_manager" 162 | 163 | # visible atomic preferences 164 | enable_missing_file_warning: bpy.props.BoolProperty( 165 | description="Display a warning on startup if Atomic detects " 166 | "missing files in your project", 167 | default=True 168 | ) 169 | 170 | enable_support_me_popup: bpy.props.BoolProperty( 171 | description="Occasionally display a popup asking if you would " 172 | "like to support Remington Creative", 173 | default=True 174 | ) 175 | 176 | include_fake_users: bpy.props.BoolProperty( 177 | description="Include data-blocks with only fake users in unused " 178 | "data detection", 179 | default=False 180 | ) 181 | 182 | enable_pie_menu_ui: bpy.props.BoolProperty( 183 | description="Enable the Atomic pie menu UI, so you can clean " 184 | "your project from anywhere.", 185 | default=True, 186 | update=update_pie_menu_hotkeys 187 | ) 188 | 189 | # hidden atomic preferences 190 | pie_menu_type: bpy.props.StringProperty( 191 | default="D" 192 | ) 193 | 194 | pie_menu_alt: bpy.props.BoolProperty( 195 | default=False 196 | ) 197 | 198 | pie_menu_any: bpy.props.BoolProperty( 199 | default=False 200 | ) 201 | 202 | pie_menu_ctrl: bpy.props.BoolProperty( 203 | default=False 204 | ) 205 | 206 | pie_menu_oskey: bpy.props.BoolProperty( 207 | default=False 208 | ) 209 | 210 | pie_menu_shift: bpy.props.BoolProperty( 211 | default=False 212 | ) 213 | 214 | last_popup_day: bpy.props.FloatProperty( 215 | default=0 216 | ) 217 | 218 | # add-on updater properties 219 | auto_check_update: bpy.props.BoolProperty( 220 | name="Auto-check for Update", 221 | description="If enabled, auto-check for updates using an interval", 222 | default=True, 223 | ) 224 | 225 | updater_intrval_months: bpy.props.IntProperty( 226 | name='Months', 227 | description="Number of months between checking for updates", 228 | default=0, 229 | min=0, 230 | max=6 231 | ) 232 | updater_intrval_days: bpy.props.IntProperty( 233 | name='Days', 234 | description="Number of days between checking for updates", 235 | default=7, 236 | min=0, 237 | ) 238 | updater_intrval_hours: bpy.props.IntProperty( 239 | name='Hours', 240 | description="Number of hours between checking for updates", 241 | default=0, 242 | min=0, 243 | max=23 244 | ) 245 | updater_intrval_minutes: bpy.props.IntProperty( 246 | name='Minutes', 247 | description="Number of minutes between checking for updates", 248 | default=0, 249 | min=0, 250 | max=59 251 | ) 252 | 253 | def draw(self, context): 254 | layout = self.layout 255 | 256 | split = layout.split() 257 | 258 | # left column 259 | col = split.column() 260 | 261 | # enable missing file warning toggle 262 | col.prop( 263 | self, 264 | "enable_missing_file_warning", 265 | text="Show Missing File Warning" 266 | ) 267 | 268 | # enable support me popup toggle 269 | col.prop( 270 | self, 271 | "enable_support_me_popup", 272 | text="Show \"Support Me\" Popup" 273 | ) 274 | 275 | # right column 276 | col = split.column() 277 | 278 | # ignore fake users toggle 279 | col.prop( 280 | self, 281 | "include_fake_users", 282 | text="Include Fake Users" 283 | ) 284 | 285 | # pie menu settings 286 | pie_split = col.split(factor=0.55) # nice 287 | 288 | # enable pie menu ui toggle 289 | pie_split.prop( 290 | self, 291 | "enable_pie_menu_ui", 292 | text="Enable Pie Menu" 293 | ) 294 | 295 | # put the property in a row so it can be disabled 296 | pie_row = pie_split.row() 297 | pie_row.enabled = self.enable_pie_menu_ui 298 | 299 | if pie_row.enabled: 300 | # keymap item that contains our pie menu hotkey 301 | # note: keymap item index hardcoded with an index -- may be 302 | # dangerous if more keymap items are added 303 | kmi = bpy.context.window_manager.keyconfigs.addon.keymaps[ 304 | 'Window'].keymap_items[0] 305 | 306 | # hotkey property 307 | pie_row.prop( 308 | kmi, 309 | "type", 310 | text="", 311 | full_event=True 312 | ) 313 | 314 | # update hotkey preferences 315 | self.pie_menu_type = kmi.type 316 | self.pie_menu_any = kmi.any 317 | self.pie_menu_alt = kmi.alt 318 | self.pie_menu_ctrl = kmi.ctrl 319 | self.pie_menu_oskey = kmi.oskey 320 | self.pie_menu_shift = kmi.shift 321 | 322 | separator = layout.row() # extra space 323 | 324 | # add-on updater box 325 | addon_updater_ops.update_settings_ui(self, context) 326 | 327 | # update config with any new preferences 328 | copy_prefs_to_config(None, None) 329 | 330 | 331 | reg_list = [ATOMIC_PT_preferences_panel] 332 | keymaps = [] 333 | 334 | 335 | def register(): 336 | for cls in reg_list: 337 | register_class(cls) 338 | 339 | # make sure global preferences are updated on registration 340 | copy_prefs_to_config(None, None) 341 | 342 | # update keymaps 343 | add_pie_menu_hotkeys() 344 | 345 | 346 | def unregister(): 347 | for cls in reg_list: 348 | unregister_class(cls) 349 | 350 | remove_pie_menu_hotkeys() 351 | -------------------------------------------------------------------------------- /ui/stats_panel_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the user interface for Atomic's statistics subpanel. 22 | 23 | The statistics panel is nested in the main Atomic Data Manager panel. This 24 | panel contains statistics about the Blender file and each data category in 25 | it. 26 | 27 | """ 28 | 29 | import bpy 30 | from bpy.utils import register_class 31 | from bpy.utils import unregister_class 32 | from atomic_data_manager.stats import count 33 | from atomic_data_manager.stats import misc 34 | from atomic_data_manager.ui.utils import ui_layouts 35 | 36 | 37 | # Atomic Data Manager Statistics SubPanel 38 | class ATOMIC_PT_stats_panel(bpy.types.Panel): 39 | """The Atomic Data Manager \"Stats for Nerds\" panel""" 40 | bl_idname = "ATOMIC_PT_stats_panel" 41 | bl_label = "Stats for Nerds" 42 | bl_space_type = "PROPERTIES" 43 | bl_region_type = "WINDOW" 44 | bl_parent_id = "ATOMIC_PT_main_panel" 45 | 46 | def draw(self, context): 47 | layout = self.layout 48 | atom = bpy.context.scene.atomic 49 | 50 | # categories selector / header 51 | row = layout.row() 52 | row.label(text="Categories:") 53 | row.prop(atom, "stats_mode", expand=True, icon_only=True) 54 | 55 | # statistics box 56 | box = layout.box() 57 | 58 | # overview statistics 59 | if atom.stats_mode == 'OVERVIEW': 60 | 61 | # category header label 62 | row = box.row() 63 | row.label(text="Overview", icon='FILE') 64 | 65 | # blender project file size statistic 66 | row = box.row() 67 | row.label(text="Blend File Size: " + misc.blend_size()) 68 | 69 | # cateogry statistics 70 | split = box.split() 71 | 72 | # left column 73 | col = split.column() 74 | 75 | # left column category labels 76 | col.label(text="Collections") 77 | col.label(text="Lights") 78 | col.label(text="Node Groups") 79 | col.label(text="Textures") 80 | 81 | col = split.column() 82 | 83 | # collection count 84 | col.label(text=str(count.collections())) 85 | 86 | # light count 87 | col.label(text=str(count.lights())) 88 | 89 | # node group count 90 | col.label(text=str(count.node_groups())) 91 | 92 | # texture count 93 | col.label(text=str(count.textures())) 94 | 95 | # right column 96 | col = split.column() 97 | 98 | # right column category labels 99 | col.label(text="Images") 100 | col.label(text="Materials") 101 | col.label(text="Particles") 102 | col.label(text="Worlds") 103 | 104 | col = split.column() 105 | 106 | # image count 107 | col.label(text=str(count.images())) 108 | 109 | # material count 110 | col.label(text=str(count.materials())) 111 | 112 | # particle system count 113 | col.label(text=str(count.particles())) 114 | 115 | # world count 116 | col.label(text=str(count.worlds())) 117 | 118 | # collection statistics 119 | elif atom.stats_mode == 'COLLECTIONS': 120 | 121 | # category header label 122 | row = box.row() 123 | row.label(text="Collections", icon='GROUP') 124 | 125 | split = box.split() 126 | 127 | # total and placeholder count 128 | col = split.column() 129 | 130 | col.label( 131 | text="Total: {0}".format(count.collections()) 132 | ) 133 | 134 | # col.label(text="Placeholder") # TODO: remove placeholder 135 | 136 | # unused and unnamed count 137 | col = split.column() 138 | 139 | col.label( 140 | text="Unused: {0}".format(count.collections_unused()) 141 | ) 142 | 143 | col.label( 144 | text="Unnamed: {0}".format(count.collections_unnamed()) 145 | ) 146 | 147 | # image statistics 148 | elif atom.stats_mode == 'IMAGES': 149 | 150 | # category header label 151 | row = box.row() 152 | row.label(text="Images", icon='IMAGE_DATA') 153 | 154 | split = box.split() 155 | 156 | # total and missing count 157 | col = split.column() 158 | 159 | col.label( 160 | text="Total: {0}".format(count.images()) 161 | ) 162 | 163 | col.label( 164 | text="Missing: {0}".format(count.images_missing()) 165 | ) 166 | 167 | # unused and unnamed count 168 | col = split.column() 169 | 170 | col.label( 171 | text="Unused: {0}".format(count.images_unused()) 172 | ) 173 | col.label( 174 | text="Unnamed: {0}".format(count.images_unnamed()) 175 | ) 176 | 177 | # light statistics 178 | elif atom.stats_mode == 'LIGHTS': 179 | row = box.row() 180 | row.label(text="Lights", icon='LIGHT') 181 | 182 | split = box.split() 183 | 184 | # total and placeholder count 185 | col = split.column() 186 | 187 | col.label( 188 | text="Total: {0}".format(count.lights()) 189 | ) 190 | 191 | # col.label(text="Placeholder") # TODO: remove placeholder 192 | 193 | # unused and unnamed count 194 | col = split.column() 195 | 196 | col.label( 197 | text="Unused: {0}".format(count.lights_unused()) 198 | ) 199 | 200 | col.label( 201 | text="Unnamed: {0}".format(count.lights_unnamed()) 202 | ) 203 | 204 | # material statistics 205 | elif atom.stats_mode == 'MATERIALS': 206 | 207 | # category header label 208 | row = box.row() 209 | row.label(text="Materials", icon='MATERIAL') 210 | 211 | split = box.split() 212 | 213 | # total and placeholder count 214 | col = split.column() 215 | 216 | col.label( 217 | text="Total: {0}".format(count.materials()) 218 | ) 219 | 220 | # col.label(text="Placeholder") # TODO: remove placeholder 221 | 222 | # unused and unnamed count 223 | col = split.column() 224 | 225 | col.label( 226 | text="Unused: {0}".format(count.materials_unused()) 227 | ) 228 | 229 | col.label( 230 | text="Unnamed: {0}".format(count.materials_unnamed()) 231 | ) 232 | 233 | # object statistics 234 | elif atom.stats_mode == 'OBJECTS': 235 | 236 | # category header label 237 | row = box.row() 238 | row.label(text="Objects", icon='OBJECT_DATA') 239 | 240 | # total count 241 | split = box.split() 242 | col = split.column() 243 | 244 | col.label( 245 | text="Total: {0}".format(count.objects()) 246 | ) 247 | 248 | # unnamed count 249 | col = split.column() 250 | 251 | col.label( 252 | text="Unnamed: {0}".format(count.objects_unnamed()) 253 | ) 254 | 255 | # node group statistics 256 | elif atom.stats_mode == 'NODE_GROUPS': 257 | 258 | # category header label 259 | row = box.row() 260 | row.label(text="Node Groups", icon='NODETREE') 261 | 262 | split = box.split() 263 | 264 | # total and placeholder count 265 | col = split.column() 266 | 267 | col.label( 268 | text="Total: {0}".format(count.node_groups()) 269 | ) 270 | 271 | # col.label(text="Placeholder") # TODO: remove placeholder 272 | 273 | # unused and unnamed count 274 | col = split.column() 275 | col.label( 276 | text="Unused: {0}".format(count.node_groups_unused()) 277 | ) 278 | col.label( 279 | text="Unnamed: {0}".format(count.node_groups_unnamed()) 280 | ) 281 | 282 | # particle statistics 283 | elif atom.stats_mode == 'PARTICLES': 284 | 285 | # category header label 286 | row = box.row() 287 | row.label(text="Particle Systems", icon='PARTICLES') 288 | 289 | split = box.split() 290 | 291 | # total and placeholder count 292 | col = split.column() 293 | 294 | col.label( 295 | text="Total: {0}".format(count.particles()) 296 | ) 297 | 298 | # col.label(text="Placeholder") # TODO: remove placeholder 299 | 300 | # unused and unnamed count 301 | col = split.column() 302 | 303 | col.label( 304 | text="Unused: {0}".format(count.particles_unused()) 305 | ) 306 | 307 | col.label( 308 | text="Unnamed: {0}".format(count.particles_unnamed()) 309 | ) 310 | 311 | # texture statistics 312 | elif atom.stats_mode == 'TEXTURES': 313 | row = box.row() 314 | row.label(text="Textures", icon='TEXTURE') 315 | 316 | split = box.split() 317 | 318 | # total and placeholder count 319 | col = split.column() 320 | 321 | col.label( 322 | text="Total: {0}".format(count.textures()) 323 | ) 324 | 325 | # col.label(text="Placeholder") # TODO: remove placeholder 326 | 327 | # unused and unnamed count 328 | col = split.column() 329 | 330 | col.label( 331 | text="Unused: {0}".format(count.textures_unused()) 332 | ) 333 | 334 | col.label( 335 | text="Unnamed: {0}".format(count.textures_unnamed()) 336 | ) 337 | 338 | # world statistics 339 | elif atom.stats_mode == 'WORLDS': 340 | row = box.row() 341 | row.label(text="Worlds", icon='WORLD') 342 | 343 | split = box.split() 344 | 345 | # total and placeholder count 346 | col = split.column() 347 | 348 | col.label( 349 | text="Total: {0}".format(count.worlds()) 350 | ) 351 | 352 | # # col.label(text="Placeholder") # TODO: remove placeholder 353 | 354 | # unused and unnamed count 355 | col = split.column() 356 | 357 | col.label( 358 | text="Unused: {0}".format(count.worlds_unused()) 359 | ) 360 | 361 | col.label( 362 | text="Unnamed: {0}".format(count.worlds_unnamed()) 363 | ) 364 | 365 | 366 | reg_list = [ATOMIC_PT_stats_panel] 367 | 368 | 369 | def register(): 370 | for cls in reg_list: 371 | register_class(cls) 372 | 373 | 374 | def unregister(): 375 | for cls in reg_list: 376 | unregister_class(cls) 377 | -------------------------------------------------------------------------------- /ui/support_me_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains the user interface and some helper functions for the 22 | support Remington Creative popup. 23 | 24 | """ 25 | 26 | import bpy 27 | import time 28 | from bpy.utils import register_class 29 | from bpy.utils import unregister_class 30 | from bpy.app.handlers import persistent 31 | from atomic_data_manager import config 32 | from atomic_data_manager.ui import preferences_ui 33 | 34 | 35 | def get_current_day(): 36 | # returns the current day since the start of the computer clock 37 | seconds_per_day = 86400 38 | return int(time.time() / seconds_per_day) 39 | 40 | 41 | def update_enable_show_support_me_popup(self, context): 42 | # copy the inverse of the stop show support popup property to Atomic's 43 | # enable support me popup preference 44 | preferences_ui.set_enable_support_me_popup( 45 | not self.stop_showing_support_popup) 46 | 47 | 48 | @persistent 49 | def show_support_me_popup(dummy=None): 50 | # shows the support me popup if the 5 day interval has expired and the 51 | # enable support me popup preference is enabled 52 | 53 | popup_interval = 5 # days 54 | 55 | current_day = get_current_day() 56 | next_day = config.last_popup_day + popup_interval 57 | 58 | if config.enable_support_me_popup and current_day >= next_day: 59 | preferences_ui.set_last_popup_day(current_day) 60 | bpy.ops.atomic.show_support_me('INVOKE_DEFAULT') 61 | 62 | 63 | # Atomic Data Manager Support Me Popup Operator 64 | class ATOMIC_OT_support_me_popup(bpy.types.Operator): 65 | """Displays the Atomic \"Support Me\" popup""" 66 | bl_idname = "atomic.show_support_me" 67 | bl_label = "Like Atomic Data Manager?" 68 | bl_options = {'REGISTER', 'UNDO'} 69 | 70 | stop_showing_support_popup: bpy.props.BoolProperty( 71 | default=False, 72 | update=update_enable_show_support_me_popup 73 | ) 74 | 75 | def draw(self, context): 76 | layout = self.layout 77 | 78 | # call to action label 79 | col = layout.column(align=True) 80 | col.label( 81 | text="Consider supporting our free software development!" 82 | ) 83 | 84 | separator = layout.separator() # extra space 85 | 86 | # never show again toggle 87 | row = layout.row() 88 | row.prop( 89 | self, "stop_showing_support_popup", text="Never Show Again" 90 | ) 91 | 92 | # support remington creative button 93 | row = layout.row() 94 | row.scale_y = 2 95 | row.operator( 96 | "atomic.open_support_me", 97 | text="Support Remington Creative", 98 | icon="FUND" 99 | ) 100 | 101 | def execute(self, context): 102 | return {'FINISHED'} 103 | 104 | def invoke(self, context, event): 105 | wm = context.window_manager 106 | return wm.invoke_props_dialog(self) 107 | 108 | 109 | reg_list = [ATOMIC_OT_support_me_popup] 110 | 111 | 112 | def register(): 113 | for cls in reg_list: 114 | register_class(cls) 115 | 116 | bpy.app.handlers.load_post.append(show_support_me_popup) 117 | 118 | # reset day counter if it equals zero of if it is in the future 119 | if config.last_popup_day == 0 \ 120 | or config.last_popup_day > get_current_day(): 121 | preferences_ui.set_last_popup_day(get_current_day()) 122 | 123 | 124 | def unregister(): 125 | for cls in reg_list: 126 | unregister_class(cls) 127 | 128 | bpy.app.handlers.load_post.remove(show_support_me_popup) 129 | -------------------------------------------------------------------------------- /ui/utils/ui_layouts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Remington Creative 3 | 4 | This file is part of Atomic Data Manager. 5 | 6 | Atomic Data Manager is free software: you can redistribute 7 | it and/or modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | Atomic Data Manager is distributed in the hope that it will 12 | be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 13 | of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with Atomic Data Manager. If not, see . 18 | 19 | --- 20 | 21 | This file contains basic UI layouts for the Atomic add-on that can be 22 | used throughout the interface. 23 | 24 | """ 25 | 26 | import bpy 27 | 28 | 29 | def box_list(layout, title=None, items=None, columns=2, icon=None): 30 | # a title label followed by a box that contains a two column list of 31 | # items, each of which is preceded by a uniform icon that does not 32 | # change depending on the objects type 33 | 34 | # box list title 35 | row = layout.row() # extra row for additional spacing 36 | 37 | if title is not None: 38 | row = layout.row() 39 | row.label(text=title) 40 | 41 | box = layout.box() 42 | 43 | # if the list has elements 44 | if items is not None and len(items) != 0: 45 | 46 | # display the list 47 | flow = box.column_flow(columns=columns) 48 | for item in items: 49 | if icon is not None: 50 | flow.label(text=item, icon=icon) 51 | else: 52 | flow.label(text=item) 53 | 54 | # if the list has no elements 55 | else: 56 | 57 | # display the none label 58 | row = box.row() 59 | row.enabled = False 60 | row.label(text="none") 61 | 62 | 63 | def box_list_diverse(layout, title, items, columns=2): 64 | # a title label followed by a box that contains a two column list of 65 | # items, each of which is preceded by an icon that changes depending 66 | # on the type of object that is being listed 67 | 68 | # box list title 69 | row = layout.row() # extra row for additional spacing 70 | row = layout.row() 71 | row.label(text=title) 72 | box = layout.box() 73 | 74 | # if the list has elements 75 | if len(items) != 0: 76 | 77 | # display the list 78 | flow = box.column_flow(columns=columns) 79 | objects = bpy.data.objects 80 | for item in items: 81 | if objects[item].type == 'ARMATURE': 82 | flow.label(text=item, icon="OUTLINER_OB_ARMATURE") 83 | 84 | elif objects[item].type == 'CAMERA': 85 | flow.label(text=item, icon="OUTLINER_OB_CAMERA") 86 | 87 | elif objects[item].type == 'CURVE': 88 | flow.label(text=item, icon="OUTLINER_OB_CURVE") 89 | 90 | elif objects[item].type == 'EMPTY': 91 | flow.label(text=item, icon="OUTLINER_OB_EMPTY") 92 | 93 | elif objects[item].type == 'FONT': 94 | flow.label(text=item, icon="OUTLINER_OB_FONT") 95 | 96 | elif objects[item].type == 'GPENCIL': 97 | flow.label(text=item, icon="OUTLINER_OB_GREASEPENCIL") 98 | 99 | elif objects[item].type == 'LATTICE': 100 | flow.label(text=item, icon="OUTLINER_OB_LATTICE") 101 | 102 | elif objects[item].type == 'LIGHT': 103 | flow.label(text=item, icon="OUTLINER_OB_LIGHT") 104 | 105 | elif objects[item].type == 'LIGHT_PROBE': 106 | flow.label(text=item, icon="OUTLINER_OB_LIGHTPROBE") 107 | 108 | elif objects[item].type == 'MESH': 109 | flow.label(text=item, icon="OUTLINER_OB_MESH") 110 | 111 | elif objects[item].type == 'META': 112 | flow.label(text=item, icon="OUTLINER_OB_META") 113 | 114 | elif objects[item].type == 'SPEAKER': 115 | flow.label(text=item, icon="OUTLINER_OB_SPEAKER") 116 | 117 | elif objects[item].type == 'SURFACE': 118 | flow.label(text=item, icon="OUTLINER_OB_SURFACE") 119 | 120 | # if the object doesn't fit any of the previous types 121 | else: 122 | flow.label(text=item, icon="QUESTION") 123 | 124 | # if the list has no elements 125 | else: 126 | 127 | # display the none label 128 | row = box.row() 129 | row.enabled = False 130 | row.label(text="none") 131 | 132 | 133 | def inspect_header(layout, atom_prop, data): 134 | # a single column containing a search property and basic data 135 | # manipulation functions that appears at the top of all inspect data 136 | # set dialogs 137 | 138 | atom = bpy.context.scene.atomic 139 | 140 | # exterior box and prop search for data-blocks 141 | col = layout.column(align=True) 142 | box = col.box() 143 | row = box.row() 144 | split = row.split() 145 | split.prop_search(atom, atom_prop, bpy.data, data, text="") 146 | 147 | # convert the data set string into an actual data set reference 148 | data = getattr(bpy.data, data) 149 | 150 | # get the string value of the string property 151 | text_field = getattr(atom, atom_prop) 152 | 153 | # determine whether or not the text entered in the string property 154 | # is a valid key 155 | is_valid_key = text_field in data.keys() 156 | 157 | # determine whether or not the piece of data is using a fake user 158 | has_fake_user = is_valid_key and data[text_field].use_fake_user 159 | 160 | # buttons that follow the prop search 161 | split = row.split() 162 | row = split.row(align=True) 163 | 164 | # disable the buttons if the key in the search property is invalid 165 | row.enabled = is_valid_key 166 | 167 | # toggle fake user button (do not show for collections) 168 | # icon and depression changes depending on whether or not the object 169 | # is using a fake user 170 | if data != bpy.data.collections: 171 | 172 | # has fake user 173 | if has_fake_user: 174 | row.operator( 175 | "atomic.toggle_fake_user", 176 | text="", 177 | icon="FAKE_USER_ON", 178 | depress=True 179 | ) 180 | 181 | # does not have fake user 182 | else: 183 | row.operator( 184 | "atomic.toggle_fake_user", 185 | text="", 186 | icon="FAKE_USER_OFF", 187 | depress=False 188 | ) 189 | 190 | # duplicate button 191 | row.operator( 192 | "atomic.inspection_duplicate", 193 | text="", 194 | icon="DUPLICATE" 195 | ) 196 | 197 | # replace button (do not show for collections) 198 | if data != bpy.data.collections: 199 | row.operator( 200 | "atomic.replace", 201 | text="", 202 | icon="UV_SYNC_SELECT" 203 | ) 204 | 205 | # rename button 206 | row.operator( 207 | "atomic.rename", 208 | text="", 209 | icon="GREASEPENCIL" 210 | ) 211 | 212 | # delete button 213 | row.operator( 214 | "atomic.inspection_delete", 215 | text="", 216 | icon="TRASH" 217 | ) 218 | 219 | 220 | def number_suffix(text, number): 221 | # returns the text properly formatted as a suffix 222 | # e.g. passing in "hello" and "100" will result in "hello (100)" 223 | 224 | return text + " ({0})".format(number) if int(number) != 0 else text 225 | --------------------------------------------------------------------------------