├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE.md ├── README.md ├── __init__.py ├── addon.py ├── bl_utils.py ├── c_utils.py ├── collection_utils.py ├── compatibility_fixes.py ├── constants.py ├── debug_utils.py ├── ed_base.py ├── ed_hpanel_group.py ├── ed_macro.py ├── ed_menu.py ├── ed_modal.py ├── ed_panel_group.py ├── ed_pie_menu.py ├── ed_popup.py ├── ed_property.py ├── ed_stack_key.py ├── ed_sticky_key.py ├── examples ├── 3d_view_numpad_pie.json ├── 3d_view_trackball_rotate_macro.json ├── context_sensitive_menu.json ├── mesh_connect_2_vertices_macro.json ├── mesh_edge_crease_and_bevel_weight_properties.json ├── mesh_grid_fill_modal.json ├── mesh_lasso_dissolve_macro.json ├── mesh_select_mode_pie.json ├── object_mode_pie.json ├── toolbar_top.json └── window_area_pie.json ├── extra_operators.py ├── icons ├── p1.png ├── p2.png ├── p3.png ├── p4.png ├── p6.png ├── p7.png ├── p8.png ├── p9.png ├── pA.png ├── pB.png ├── pChord.png ├── pDouble.png ├── pHold.png ├── pPress.png └── pTweak.png ├── keymap_helper.py ├── layout_helper.py ├── macro_utils.py ├── modal_utils.py ├── operator_utils.py ├── operators.py ├── overlay.py ├── panel_utils.py ├── pme.py ├── preferences.py ├── previews_helper.py ├── property_utils.py ├── screen_utils.py ├── scripts ├── autorun │ └── functions.py ├── command_area_join.py ├── command_area_move.py ├── command_area_split.py ├── command_context_sensitive_menu.py ├── command_hello_world.py ├── command_localview.py ├── command_panel.py ├── command_return_value.py └── custom_hello_world.py ├── selection_state.py ├── types.py ├── ui.py ├── ui_utils.py └── utils.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Pluglug] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report bugs in Pie Menu Editor (PME) 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: Pluglug 7 | --- 8 | 9 | ## Bug Overview 10 | - **Summary**: Provide a brief explanation of the issue. 11 | 12 | ## Steps to Reproduce 13 | 1. Describe the actions leading to the issue. 14 | 2. Include specific steps, if applicable. 15 | 16 | ## Expected Behavior 17 | - Describe what should have happened instead. 18 | 19 | ## Environment 20 | - Blender Version: 21 | - PME Version: 22 | - OS: 23 | 24 | ## Supporting Materials 25 | - Please attach **screenshots, videos, or logs** to help us understand the issue. 26 | 27 | ## Additional Information 28 | - Include any other relevant details here. 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------------------- 2 | # Python (General) 3 | # ---------------------------------------------------------------------------------------- 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # .python-version 87 | 88 | # pipenv 89 | #Pipfile.lock 90 | 91 | # poetry 92 | #poetry.lock 93 | 94 | # pdm 95 | .pdm.toml 96 | 97 | # PEP 582 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder / Rope 117 | .spyderproject 118 | .spyproject 119 | .ropeproject 120 | 121 | # mkdocs 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre 130 | .pyre/ 131 | 132 | # pytype 133 | .pytype/ 134 | 135 | # Cython debug symbols 136 | cython_debug/ 137 | 138 | # ---------------------------------------------------------------------------------------- 139 | # Blender Add-on Development 140 | # ---------------------------------------------------------------------------------------- 141 | 142 | # Blender backup files 143 | *.blend1 144 | *.blend2 145 | *.blend@ 146 | *.blend# 147 | 148 | # Blender autosave or temporary files 149 | *.autosave 150 | *_autosave.blend 151 | *.tmp 152 | 153 | # Packaged add-on distributions 154 | *.zip 155 | 156 | # ---------------------------------------------------------------------------------------- 157 | # C / C++ Artifacts 158 | # ---------------------------------------------------------------------------------------- 159 | *.obj 160 | *.o 161 | *.exe 162 | *.dll 163 | *.pdb 164 | *.lib 165 | *.a 166 | *.la 167 | *.lo 168 | *.slo 169 | *.dylib 170 | *.so 171 | 172 | # Common build system outputs 173 | CMakeFiles/ 174 | CMakeCache.txt 175 | cmake_install.cmake 176 | Makefile 177 | *.mk 178 | *.out 179 | 180 | # IDE project files 181 | *.vcxproj 182 | *.vcxproj.user 183 | *.vcxproj.filters 184 | *.vscode/ 185 | *.sln 186 | *.suo 187 | *.xcodeproj/ 188 | *.xcworkspace/ 189 | 190 | # ---------------------------------------------------------------------------------------- 191 | # OS / Editor Artifacts 192 | # ---------------------------------------------------------------------------------------- 193 | 194 | # macOS / Windows 195 | .DS_Store 196 | Thumbs.db 197 | *.swp 198 | 199 | # Visual Studio Code 200 | .vscode/ 201 | 202 | # JetBrains IDE 203 | .idea/ 204 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016-2024 roaoao 2 | Copyright (C) 2024 Pluglug and contributors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pie Menu Editor Fork 2 | 3 | ## Overview 4 | This repository is a community-driven maintenance project for the Blender addon "Pie Menu Editor (PME)" Under the terms of the GPL license, we are exploring a sustainable development model and focusing on expanding PME’s reach to a broader user base. 5 | 6 | PME was originally developed by [roaoao](https://github.com/roaoao) in 2016 and made available on the [Blender Market](https://blendermarket.com/products/pie-menu-editor) and [Gumroad](https://roaoao.gumroad.com/l/pie_menu_editor). 7 | 8 | With PME, users could easily extend their custom UI and enhance their efficiency when working with Blender. Its ease of use and extensibility have consistently earned high praise from the user community. 9 | 10 | Our goal is to ensure the long-term stability and growth of this exceptional tool within the Blender ecosystem. 11 | 12 | --- 13 | 14 | **Documentation (WIP)**: 15 | For more details, guides, and instructions, please visit our documentation site: [PME Docs](https://pluglug.github.io/pme-docs/) 16 | 17 | --- 18 | 19 | ## Acknowledgment 20 | This project is a fork of the original Pie Menu Editor created by [roaoao](https://github.com/roaoao). 21 | We deeply respect roaoao's contributions and acknowledge their expertise in creating such a versatile tool. 22 | The original copyrights remain with roaoao, and this repository is maintained in accordance with the terms of the GPL license. 23 | 24 | --- 25 | 26 | ## Repository Goals 27 | - **Maintain Blender Version Compatibility** 28 | Ensure PME continues to function seamlessly with the latest Blender updates. 29 | - **Enhance Stability** 30 | Address community-reported issues to provide a more reliable experience. 31 | - **Implement Community Features** 32 | Add new functionalities based on user feedback while maintaining PME's core design. 33 | 34 | --- 35 | 36 | ## How to Contribute 37 | 1. **Report Issues** 38 | Submit bugs and improvement suggestions through the [Issues](../../issues) section. 39 | 2. **Pull Requests** 40 | Fork the repository, make your changes, and create a pull request for review. 41 | 3. **Community Discussion** 42 | Share ideas and collaborate with other participants in the [Discussions](../../discussions) section. 43 | 44 | **Trusted Contributors** 45 | Active contributors may be invited to join as collaborators with expanded project management permissions. 46 | 47 | --- 48 | 49 | ## License 50 | This project is licensed under the [GNU General Public License v3.0](./LICENSE.md). 51 | All original rights to Pie Menu Editor are retained by roaoao, the original developer. 52 | 53 | --- 54 | 55 | ## Disclaimer 56 | This repository and its maintainers are not affiliated with or endorsed by roaoao, the original creator of Pie Menu Editor. 57 | 58 | The provided modification scripts aim to maintain compatibility and improve usability, but we make no guarantees regarding the software's functionality, safety, or compliance with applicable laws. 59 | 60 | Any maintenance work conducted through this repository is performed in good faith to support PME's existing user base. The repository maintainers will promptly comply with any requests from the original creator to modify or remove content. 61 | 62 | --- 63 | 64 | ## Contact 65 | For questions or suggestions, please use the [Issues](../../issues) section. 66 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Pie Menu Editor", 3 | "author": "roaoao, pluglug", 4 | "version": (1, 18, 8), 5 | "blender": (3, 2, 0), 6 | "warning": "", 7 | "tracker_url": "http://blenderartists.org/forum/showthread.php?392910", 8 | # "wiki_url": ( 9 | # "https://archive.blender.org/wiki/2015/index.php/User:Raa/Addons/Pie_Menu_Editor/"), 10 | "doc_url": "https://pluglug.github.io/pme-docs", 11 | "category": "User Interface" 12 | } 13 | 14 | import bpy 15 | import _bpy 16 | from bpy.app.handlers import persistent 17 | import sys 18 | import inspect 19 | from .debug_utils import * 20 | 21 | MODULES = ( 22 | "addon", 23 | "pme", 24 | "c_utils", 25 | "previews_helper", 26 | "constants", 27 | "utils", 28 | "debug_utils", 29 | "bl_utils", 30 | "compatibility_fixes", 31 | "operator_utils", 32 | "property_utils", 33 | "layout_helper", 34 | "overlay", 35 | "modal_utils", 36 | "macro_utils", 37 | "ui", 38 | "panel_utils", 39 | "screen_utils", 40 | "selection_state", 41 | "keymap_helper", 42 | "collection_utils", 43 | "operators", 44 | "extra_operators", 45 | "ui_utils", 46 | "types", 47 | "ed_base", 48 | "ed_pie_menu", 49 | "ed_menu", 50 | "ed_popup", 51 | "ed_stack_key", 52 | "ed_sticky_key", 53 | "ed_macro", 54 | "ed_modal", 55 | "ed_panel_group", 56 | "ed_hpanel_group", 57 | "ed_property", 58 | "preferences", 59 | ) 60 | 61 | CLASSES = [] 62 | 63 | 64 | def get_classes(): 65 | ret = set() 66 | bpy_struct = bpy.types.bpy_struct 67 | cprop = bpy.props.CollectionProperty 68 | pprop = bpy.props.PointerProperty 69 | pdtype = getattr(bpy.props, "_PropertyDeferred", tuple) 70 | mems = set() 71 | mem_data = [] 72 | for mod in MODULES: 73 | mod = sys.modules["%s.%s" % (__name__, mod)] 74 | # mod = sys.modules[mod] 75 | for name, mem in inspect.getmembers(mod): 76 | if inspect.isclass(mem) and issubclass(mem, bpy_struct) and \ 77 | mem not in mems: 78 | 79 | mems.add(mem) 80 | classes = [] 81 | 82 | if hasattr(mem, "__annotations__"): 83 | for pname, pd in mem.__annotations__.items(): 84 | if not isinstance(pd, pdtype): 85 | continue 86 | 87 | pfunc = getattr(pd, "function", None) or pd[0] 88 | pkeywords = pd.keywords if hasattr(pd, "keywords") \ 89 | else pd[1] 90 | if pfunc is cprop or pfunc is pprop: 91 | classes.append(pkeywords["type"]) 92 | 93 | if not classes: 94 | ret.add(mem) 95 | else: 96 | mem_data.append( 97 | dict( 98 | mem=mem, 99 | classes=classes 100 | ) 101 | ) 102 | 103 | mems.clear() 104 | 105 | ret_post = [] 106 | if mem_data: 107 | mem_data_len = -1 108 | while len(mem_data): 109 | if len(mem_data) == mem_data_len: 110 | for data in mem_data: 111 | ret_post.append(data["mem"]) 112 | break 113 | 114 | new_mem_data = [] 115 | for data in mem_data: 116 | add = True 117 | for cls in data["classes"]: 118 | if cls not in ret and cls not in ret_post: 119 | add = False 120 | break 121 | 122 | if add: 123 | ret_post.append(data["mem"]) 124 | else: 125 | new_mem_data.append(data) 126 | 127 | mem_data_len = len(mem_data) 128 | mem_data.clear() 129 | mem_data = new_mem_data 130 | 131 | ret = list(ret) 132 | ret.extend(ret_post) 133 | return ret 134 | 135 | 136 | def register_module(): 137 | if hasattr(bpy.utils, "register_module"): 138 | bpy.utils.register_module(__name__) 139 | else: 140 | for cls in get_classes(): 141 | bpy.utils.register_class(cls) 142 | 143 | 144 | def unregister_module(): 145 | if hasattr(bpy.utils, "unregister_module"): 146 | bpy.utils.unregister_module(__name__) 147 | else: 148 | for cls in get_classes(): 149 | bpy.utils.unregister_class(cls) 150 | 151 | 152 | if not bpy.app.background: 153 | import importlib 154 | for mod in MODULES: 155 | if mod in locals(): 156 | try: 157 | importlib.reload(locals()[mod]) 158 | continue 159 | except: 160 | pass 161 | 162 | importlib.import_module("pie_menu_editor." + mod) 163 | 164 | from .addon import prefs, temp_prefs 165 | from . import property_utils 166 | from . import pme 167 | from . import compatibility_fixes 168 | from . import addon 169 | 170 | addon.VERSION = bl_info["version"] 171 | addon.BL_VERSION = bl_info["blender"] 172 | 173 | 174 | tmp_data = None 175 | re_enable_data = None 176 | tmp_filepath = None 177 | invalid_prefs = None 178 | timer = None 179 | 180 | 181 | @persistent 182 | def load_pre_handler(_): 183 | DBG_INIT and logh("Load Pre (%s)" % bpy.data.filepath) 184 | 185 | global tmp_data 186 | tmp_data = property_utils.to_dict(prefs()) 187 | 188 | global tmp_filepath 189 | tmp_filepath = bpy.data.filepath 190 | if not tmp_filepath: 191 | tmp_filepath = "__unsaved__" 192 | 193 | 194 | @persistent 195 | def load_post_handler(filepath): 196 | DBG_INIT and logh("Load Post (%s)" % filepath) 197 | 198 | global tmp_data 199 | if tmp_data is None: 200 | DBG_INIT and logw("Skip") 201 | return 202 | 203 | pr = prefs() 204 | if not bpy.data.filepath: 205 | property_utils.from_dict(pr, tmp_data) 206 | 207 | tmp_data = None 208 | 209 | if pr.missing_kms: 210 | logw(f"Missing Keymaps: {pr.missing_kms}") 211 | with bpy.context.temp_override(window=bpy.context.window_manager.windows[0]): 212 | bpy.ops.pme.wait_keymaps('INVOKE_DEFAULT') 213 | else: 214 | temp_prefs().init_tags() 215 | pr.tree.update() 216 | 217 | 218 | def on_context(): 219 | DBG_INIT and logi("On Context") 220 | 221 | bpy.app.handlers.load_pre.append(load_pre_handler) 222 | bpy.app.handlers.load_post.append(load_post_handler) 223 | 224 | pme.context.add_global("D", bpy.data) 225 | pme.context.add_global("T", bpy.types) 226 | pme.context.add_global("O", bpy.ops) 227 | pme.context.add_global("P", bpy.props) 228 | pme.context.add_global("sys", sys) 229 | pme.context.add_global("BoolProperty", bpy.props.BoolProperty) 230 | pme.context.add_global("IntProperty", bpy.props.IntProperty) 231 | pme.context.add_global("FloatProperty", bpy.props.FloatProperty) 232 | pme.context.add_global("StringProperty", bpy.props.StringProperty) 233 | pme.context.add_global("EnumProperty", bpy.props.EnumProperty) 234 | pme.context.add_global("CollectionProperty", bpy.props.CollectionProperty) 235 | pme.context.add_global("PointerProperty", bpy.props.PointerProperty) 236 | pme.context.add_global( 237 | "FloatVectorProperty", bpy.props.FloatVectorProperty) 238 | 239 | for k, v in globals().items(): 240 | if k.startswith("__"): 241 | pme.context.add_global(k, v) 242 | 243 | register_module() 244 | 245 | pr = prefs() 246 | global re_enable_data 247 | if re_enable_data is not None: 248 | if len(pr.pie_menus) == 0 and re_enable_data: 249 | property_utils.from_dict(pr, re_enable_data) 250 | re_enable_data.clear() 251 | re_enable_data = None 252 | 253 | for mod in MODULES: 254 | m = sys.modules["%s.%s" % (__name__, mod)] 255 | if hasattr(m, "register"): 256 | m.register() 257 | 258 | if pr.missing_kms: 259 | logw(f"Missing Keymaps: {pr.missing_kms}") 260 | with bpy.context.temp_override(window=bpy.context.window_manager.windows[0]): 261 | bpy.ops.pme.wait_keymaps('INVOKE_DEFAULT') 262 | 263 | else: 264 | compatibility_fixes.fix() 265 | 266 | 267 | def init_keymaps(): 268 | DBG_INIT and logi("Waiting Keymaps") 269 | 270 | pr = prefs() 271 | if not bpy.context.window_manager.keyconfigs.user: 272 | return 273 | 274 | keymaps = bpy.context.window_manager.keyconfigs.user.keymaps 275 | kms_to_remove = [] 276 | for km in pr.missing_kms.keys(): 277 | if km in keymaps: 278 | kms_to_remove.append(km) 279 | 280 | for km in kms_to_remove: 281 | pm_names = pr.missing_kms[km] 282 | for pm_name in pm_names: 283 | pr.pie_menus[pm_name].register_hotkey([km]) 284 | pr.missing_kms.pop(km, None) 285 | 286 | 287 | def on_timer(): 288 | init_keymaps() 289 | 290 | global timer 291 | pr = prefs() 292 | if not pr.missing_kms or timer.elapsed_time > 10: 293 | timer.cancel() 294 | timer = None 295 | 296 | compatibility_fixes.fix() 297 | 298 | 299 | @persistent 300 | def load_post_context(scene): 301 | bpy.app.handlers.load_post.remove(load_post_context) 302 | on_context() 303 | 304 | 305 | class PME_OT_wait_context(bpy.types.Operator): 306 | bl_idname = "pme.wait_context" 307 | bl_label = "Internal (PME)" 308 | bl_options = {'INTERNAL'} 309 | 310 | instances = [] 311 | 312 | def remove_timer(self): 313 | if self.timer: 314 | bpy.context.window_manager.event_timer_remove(self.timer) 315 | self.timer = None 316 | 317 | def modal(self, context, event): 318 | if event.type == 'TIMER': 319 | self.remove_timer() 320 | self.instances.remove(self) 321 | if self.cancelled: 322 | return {'CANCELLED'} 323 | 324 | on_context() 325 | return {'FINISHED'} 326 | 327 | return {'PASS_THROUGH'} 328 | 329 | def cancel(self, context): 330 | try: 331 | self.remove_timer() 332 | self.instances.remove(self) 333 | except: 334 | pass 335 | 336 | def execute(self, context): 337 | return {'FINISHED'} 338 | 339 | def invoke(self, context, event): 340 | self.cancelled = False 341 | self.instances.append(self) 342 | context.window_manager.modal_handler_add(self) 343 | self.timer = context.window_manager.event_timer_add( 344 | 0.01, window=context.window) 345 | return {'RUNNING_MODAL'} 346 | 347 | 348 | class PME_OT_wait_keymaps(bpy.types.Operator): 349 | bl_idname = "pme.wait_keymaps" 350 | bl_label = "Internal (PME)" 351 | bl_options = {'INTERNAL'} 352 | 353 | instances = [] 354 | 355 | def remove_timer(self): 356 | if self.timer: 357 | bpy.context.window_manager.event_timer_remove(self.timer) 358 | self.timer = None 359 | 360 | def modal(self, context, event): 361 | if event.type == 'TIMER': 362 | init_keymaps() 363 | 364 | pr = prefs() 365 | if not pr.missing_kms or self.timer.time_duration > 5: 366 | self.remove_timer() 367 | self.instances.remove(self) 368 | if self.cancelled: 369 | return {'CANCELLED'} 370 | 371 | DBG_INIT and logi("%d Missing Keymaps" % len(pr.missing_kms)) 372 | 373 | if pr.missing_kms: 374 | print( 375 | "PME: Some hotkeys cannot be registered. " 376 | "Please restart Blender") 377 | 378 | temp_prefs().init_tags() 379 | pr.tree.update() 380 | 381 | compatibility_fixes.fix() 382 | return {'FINISHED'} 383 | 384 | return {'PASS_THROUGH'} 385 | 386 | return {'PASS_THROUGH'} 387 | 388 | def cancel(self, context): 389 | try: 390 | self.remove_timer() 391 | self.instances.remove(self) 392 | except: 393 | pass 394 | DBG_INIT and logw("PME_OT_wait_keymaps Cancelled") 395 | 396 | def execute(self, context): 397 | return {'FINISHED'} 398 | 399 | def invoke(self, context, event): 400 | self.cancelled = False 401 | self.instances.append(self) 402 | context.window_manager.modal_handler_add(self) 403 | self.timer = context.window_manager.event_timer_add( 404 | 0.2, window=context.window) 405 | return {'RUNNING_MODAL'} 406 | 407 | 408 | def register(): 409 | if bpy.app.background: 410 | return 411 | 412 | DBG_INIT and logh("PME Register") 413 | 414 | if addon.check_bl_version(): 415 | if _bpy.context.window: 416 | bpy_context = bpy.context 417 | bpy.context = _bpy.context 418 | try: 419 | bpy.utils.register_class(PME_OT_wait_context) 420 | except: 421 | pass 422 | 423 | bpy.ops.pme.wait_context('INVOKE_DEFAULT') 424 | bpy.context = bpy_context 425 | else: 426 | try: 427 | bpy.utils.register_class(PME_OT_wait_keymaps) 428 | except: 429 | pass 430 | 431 | bpy.app.handlers.load_post.append(load_post_context) 432 | 433 | else: 434 | global invalid_prefs 435 | from .preferences import InvalidPMEPreferences 436 | invalid_prefs = type( 437 | "PMEPreferences", 438 | (InvalidPMEPreferences, bpy.types.AddonPreferences), {}) 439 | bpy.utils.register_class(invalid_prefs) 440 | 441 | 442 | def unregister(): 443 | if bpy.app.background: 444 | return 445 | 446 | if invalid_prefs: 447 | bpy.utils.unregister_class(invalid_prefs) 448 | return 449 | 450 | DBG_INIT and logh("PME Unregister") 451 | 452 | for op in PME_OT_wait_context.instances: 453 | op.cancelled = True 454 | 455 | for op in PME_OT_wait_keymaps.instances: 456 | op.cancelled = True 457 | 458 | global timer 459 | if timer: 460 | timer.cancel() 461 | timer = None 462 | return 463 | 464 | global re_enable_data 465 | re_enable_data = property_utils.to_dict(prefs()) 466 | 467 | for mod in reversed(MODULES): 468 | m = sys.modules["%s.%s" % (__name__, mod)] 469 | if hasattr(m, "unregister"): 470 | m.unregister() 471 | 472 | if hasattr(bpy.types.WindowManager, "pme"): 473 | delattr(bpy.types.WindowManager, "pme") 474 | 475 | if load_pre_handler in bpy.app.handlers.load_pre: 476 | bpy.app.handlers.load_pre.remove(load_pre_handler) 477 | 478 | if load_post_handler in bpy.app.handlers.load_post: 479 | bpy.app.handlers.load_post.remove(load_post_handler) 480 | 481 | if load_post_context in bpy.app.handlers.load_post: 482 | bpy.app.handlers.load_post.remove(load_post_context) 483 | 484 | unregister_module() 485 | -------------------------------------------------------------------------------- /addon.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import sys 4 | import traceback 5 | 6 | 7 | VERSION = None 8 | BL_VERSION = None 9 | ADDON_ID = os.path.basename(os.path.dirname(os.path.abspath(__file__))) 10 | ADDON_PATH = os.path.normpath(os.path.dirname(os.path.abspath(__file__))) 11 | SCRIPT_PATH = os.path.join(ADDON_PATH, "scripts/") 12 | SAFE_MODE = "--pme-safe-mode" in sys.argv 13 | ICON_ENUM_ITEMS = bpy.types.UILayout.bl_rna.functions[ 14 | "prop"].parameters["icon"].enum_items 15 | 16 | 17 | def uprefs(): 18 | return getattr(bpy.context, "user_preferences", None) or \ 19 | getattr(bpy.context, "preferences", None) 20 | 21 | 22 | def prefs(): 23 | return uprefs().addons[ADDON_ID].preferences 24 | 25 | 26 | def temp_prefs(): 27 | return getattr(getattr(bpy.context, "window_manager", None), "pme", None) 28 | 29 | 30 | def check_bl_version(version=None): 31 | version = version or BL_VERSION 32 | if version >= (2, 80, 0) and bpy.app.version < (2, 80, 0): 33 | return True 34 | 35 | return bpy.app.version >= version 36 | 37 | 38 | def check_context(): 39 | return isinstance(bpy.context, bpy.types.Context) 40 | 41 | 42 | def print_exc(text=None): 43 | if not prefs().show_error_trace: 44 | return 45 | 46 | if text is not None: 47 | print() 48 | print(">>>", text) 49 | 50 | traceback.print_exc() 51 | 52 | 53 | def is_28(): 54 | return bpy.app.version >= (2, 80, 0) 55 | 56 | 57 | def ic(icon): 58 | # Legacy_TODO: Remove or Enhance 59 | # Support for 2.79 and 2.8+ 60 | if not icon: 61 | return icon 62 | 63 | if icon in ICON_ENUM_ITEMS: 64 | return icon 65 | 66 | bl28_icons = dict( 67 | ZOOMIN="ADD", 68 | ZOOMOUT="REMOVE", 69 | ROTACTIVE="TRIA_RIGHT", 70 | ROTATE="TRIA_RIGHT_BAR", 71 | ROTATECOLLECTION="NEXT_KEYFRAME", 72 | NORMALIZE_FCURVES="ANIM_DATA", 73 | OOPS="NODETREE", 74 | SPLITSCREEN="MOUSE_MMB", 75 | GHOST="DUPLICATE", 76 | ) 77 | 78 | if icon in bl28_icons and bl28_icons[icon] in ICON_ENUM_ITEMS: 79 | return bl28_icons[icon] 80 | 81 | print("Icon not found:", icon) 82 | return 'BLENDER' 83 | 84 | 85 | def ic_rb(value): 86 | return ic('RADIOBUT_ON' if value else 'RADIOBUT_OFF') 87 | 88 | 89 | def ic_cb(value): 90 | return ic('CHECKBOX_HLT' if value else 'CHECKBOX_DEHLT') 91 | 92 | 93 | def ic_fb(value): 94 | return ic('SOLO_ON' if value else 'SOLO_OFF') 95 | 96 | 97 | def ic_eye(value): 98 | return ic('HIDE_OFF' if value else 'HIDE_ON') 99 | -------------------------------------------------------------------------------- /c_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import re 3 | from itertools import islice 4 | from ctypes import ( 5 | Structure, POINTER, cast, addressof, pointer, 6 | c_short, c_uint, c_int, c_float, c_bool, c_char, c_char_p, c_void_p 7 | ) 8 | from . import pme 9 | 10 | 11 | BKE_ST_MAXNAME = 64 12 | UI_MAX_DRAW_STR = 400 13 | UI_MAX_NAME_STR = 128 14 | UI_BLOCK_LOOP = 1 << 0 15 | UI_BLOCK_KEEP_OPEN = 1 << 8 16 | UI_BLOCK_POPUP = 1 << 9 17 | UI_BLOCK_RADIAL = 1 << 20 18 | UI_EMBOSS = 0 19 | 20 | re_field = re.compile(r"(\*?)(\w+)([\[\d\]]+)?$") 21 | 22 | 23 | def struct(name, bases=None): 24 | bases = ((Structure,) + bases) if bases else (Structure,) 25 | return type(name, bases, {}) 26 | 27 | 28 | def gen_fields(*args): 29 | ret = [] 30 | cur_tp = None 31 | 32 | def parse_str(arg): 33 | mo = re_field.match(arg) 34 | p, f, n = mo.groups() 35 | tp = POINTER(cur_tp) if p else cur_tp 36 | 37 | if n: 38 | for n in reversed(re.findall(r"\[(\d+)\]", n)): 39 | tp *= int(n) 40 | 41 | ret.append((f, tp)) 42 | 43 | bl_version = bpy.app.version 44 | for a in args: 45 | if isinstance(a, tuple): 46 | if a[0] and bl_version < a[1] or \ 47 | not a[0] and bl_version >= a[1]: 48 | continue 49 | 50 | cur_tp = a[2] 51 | for t_arg in islice(a, 3, None): 52 | parse_str(t_arg) 53 | 54 | elif isinstance(a, str): 55 | parse_str(a) 56 | 57 | else: 58 | cur_tp = a 59 | 60 | return ret 61 | 62 | 63 | def gen_pointer(obj, tp=None): 64 | if not tp: 65 | tp = Link 66 | 67 | if obj is None or isinstance(obj, int): 68 | return cast(obj, POINTER(tp)) 69 | else: 70 | return pointer(obj) 71 | 72 | 73 | class _ListBase: 74 | def __len__(self): 75 | ret = 0 76 | link_lp = cast(self.first, POINTER(Link)) 77 | while link_lp: 78 | ret += 1 79 | link_lp = link_lp.contents.next 80 | 81 | return ret 82 | 83 | def insert(self, prevlink, newlink): 84 | if prevlink: 85 | a = prevlink if isinstance(prevlink, int) else \ 86 | addressof(prevlink) 87 | prevlink_p = cast(a, POINTER(Link)).contents 88 | else: 89 | prevlink_p = None 90 | 91 | if newlink: 92 | a = newlink if isinstance(newlink, int) else \ 93 | addressof(newlink) 94 | newlink_p = cast(a, POINTER(Link)).contents 95 | else: 96 | newlink_p = None 97 | 98 | if not newlink_p: 99 | return 100 | 101 | if not self.first: 102 | self.first = self.last = addressof(newlink_p) 103 | return 104 | 105 | if not prevlink_p: 106 | newlink_p.prev = None 107 | newlink_p.next = gen_pointer(self.first) 108 | newlink_p.next.contents.prev = gen_pointer(newlink_p) 109 | self.first = addressof(newlink_p) 110 | return 111 | 112 | if self.last == addressof(prevlink_p): 113 | self.last = addressof(newlink_p) 114 | 115 | newlink_p.next = prevlink_p.next 116 | newlink_p.prev = gen_pointer(prevlink_p) 117 | prevlink_p.next = gen_pointer(newlink_p) 118 | if newlink_p.next: 119 | newlink_p.next.contents.prev = gen_pointer(newlink_p) 120 | 121 | def remove(self, link): 122 | if link: 123 | a = link if isinstance(link, int) else addressof(link) 124 | link_p = cast(a, POINTER(Link)).contents 125 | else: 126 | return 127 | 128 | if link_p.next: 129 | link_p.next.contents.prev = link_p.prev 130 | if link_p.prev: 131 | link_p.prev.contents.next = link_p.next 132 | 133 | if self.last == addressof(link_p): 134 | self.last = cast(link_p.prev, c_void_p) 135 | if self.first == addressof(link_p): 136 | self.first = cast(link_p.next, c_void_p) 137 | 138 | def find(self, idx): 139 | if idx < 0: 140 | return None 141 | 142 | link_lp = cast(self.first, POINTER(Link)) 143 | for i in range(idx): 144 | link_lp = link_lp.contents.next 145 | 146 | return link_lp.contents if link_lp else None 147 | 148 | 149 | ID = struct("ID") 150 | Link = struct("Link") 151 | ListBase = struct("ListBase", (_ListBase,)) 152 | rctf = struct("rctf") 153 | rcti = struct("rcti") 154 | uiItem = struct("uiItem") 155 | uiLayout = struct("uiLayout") 156 | uiLayoutRoot = struct("uiLayoutRoot") 157 | uiStyle = struct("uiStyle") 158 | uiFontStyle = struct("uiFontStyle") 159 | uiBlock = struct("uiBlock") 160 | uiBut = struct("uiBut") 161 | vec2s = struct("vec2s") 162 | ScrVert = struct("ScrVert") 163 | ScrArea = struct("ScrArea") 164 | ScrAreaMap = struct("ScrAreaMap") 165 | ARegion = struct("ARegion") 166 | bScreen = struct("bScreen") 167 | bContext = struct("bContext") 168 | bContext_wm = struct("bContext_wm") 169 | bContext_data = struct("bContext_data") 170 | wmWindow = struct("wmWindow") 171 | wmEventHandler_KeymapFn = struct("wmEventHandler_KeymapFn") 172 | wmEventHandler = struct("wmEventHandler") 173 | wmOperator = struct("wmOperator") 174 | # wmEvent = struct("wmEvent") 175 | 176 | # source/blender/makesdna/DNA_ID.h 177 | ID._fields_ = gen_fields( 178 | c_void_p, "*next", "*prev", 179 | ID, "*newid", 180 | c_void_p, "*lib", 181 | c_char, "name[66]", 182 | c_short, "flag", 183 | c_int, "tag", 184 | c_int, "us", 185 | c_int, "icon_id", 186 | (True, (2, 80, 0), c_int, "icon_id"), 187 | (True, (2, 80, 0), c_int, "recalc"), 188 | (True, (2, 80, 0), c_int, "pad"), 189 | c_void_p, "*properties", 190 | ) 191 | 192 | rcti._fields_ = gen_fields( 193 | c_int, "xmin", "xmax", 194 | c_int, "ymin", "ymax", 195 | ) 196 | 197 | rctf._fields_ = gen_fields( 198 | c_float, 'xmin', 'xmax', 199 | c_float, 'ymin', 'ymax', 200 | ) 201 | 202 | uiFontStyle._fields_ = gen_fields( 203 | c_short, "uifont_id", 204 | c_short, "points", 205 | c_short, "kerning", 206 | c_char, "word_wrap", 207 | c_char, "pad[5]", 208 | c_short, "italic", "bold", 209 | c_short, "shadow", 210 | c_short, "shadx", "shady", 211 | c_short, "align", 212 | c_float, "shadowalpha", 213 | c_float, "shadowcolor", 214 | ) 215 | 216 | # source/blender/makesdna/DNA_listBase.h 217 | Link._fields_ = gen_fields( 218 | Link, '*next', '*prev', 219 | ) 220 | 221 | # source/blender/makesdna/DNA_listBase.h 222 | ListBase._fields_ = gen_fields( 223 | c_void_p, "first", "last", 224 | ) 225 | 226 | uiItem._fields_ = gen_fields( 227 | c_void_p, "*next", "*prev", 228 | c_int, "type", 229 | c_int, "flag", 230 | ) 231 | 232 | # source/blender/editors/interface/interface_layout.c 233 | uiLayout._fields_ = gen_fields( 234 | uiItem, "item", 235 | uiLayoutRoot, "*root", 236 | c_void_p, "*context", 237 | (True, (2, 91, 0), uiLayout, "*parent"), 238 | ListBase, "items", 239 | (True, (2, 91, 0), c_char * UI_MAX_NAME_STR, "heading"), 240 | (True, (2, 80, 0), uiLayout, "*child_items_layout"), 241 | c_int, "x", "y", "w", "h", 242 | c_float, "scale[2]", 243 | c_short, "space", 244 | c_bool, "align", 245 | c_bool, "active", 246 | (True, (2, 91, 0), c_bool, "active_default"), 247 | (True, (2, 91, 0), c_bool, "active_init"), 248 | c_bool, "enabled", 249 | c_bool, "redalert", 250 | c_bool, "keepaspect", 251 | (True, (2, 80, 0), c_bool, "variable_size"), 252 | c_char, "alignment", 253 | ) 254 | 255 | uiLayoutRoot._fields_ = gen_fields( 256 | uiLayoutRoot, "*next", "*prev", 257 | c_int, "type", 258 | c_int, "opcontext", 259 | # (True, (2, 91, 0), c_bool, "search_only"), 260 | # (True, (2, 91, 0), ListBase, "button_groups"), 261 | c_int, "emw", "emh", 262 | c_int, "padding", 263 | c_void_p, "handlefunc", 264 | c_void_p, "*argv", 265 | uiStyle, "*style", 266 | uiBlock, "*block", 267 | uiLayout, "*layout", 268 | ) 269 | 270 | # source/blender/makesdna/DNA_userdef_types.h 271 | uiStyle._fields_ = gen_fields( 272 | uiStyle, "*next", "*prev", 273 | c_char, "name[64]", 274 | uiFontStyle, "paneltitle", 275 | uiFontStyle, "grouplabel", 276 | uiFontStyle, "widgetlabel", 277 | uiFontStyle, "widget", 278 | c_float, "panelzoom", 279 | c_short, "minlabelchars", 280 | c_short, "minwidgetchars", 281 | c_short, "columnspace", 282 | c_short, "templatespace", 283 | c_short, "boxspace", 284 | c_short, "buttonspacex", 285 | c_short, "buttonspacey", 286 | c_short, "panelspace", 287 | c_short, "panelouter", 288 | c_char, "_pad0[2]", 289 | ) 290 | 291 | uiBlock._fields_ = gen_fields( 292 | uiBlock, "*next", "*prev", 293 | ListBase, "buttons", 294 | c_void_p, "*panel", 295 | uiBlock, "*oldblock", 296 | ListBase, "butstore", 297 | ListBase, "layouts", 298 | c_void_p, "*curlayout", 299 | ListBase, "contexts", 300 | c_char * UI_MAX_NAME_STR, "name", 301 | c_float, "winmat[4][4]", 302 | rctf, "rect", 303 | c_float, "aspect", 304 | c_uint, "puphash", 305 | c_void_p, "func", 306 | c_void_p, "*func_arg1", 307 | c_void_p, "*func_arg2", 308 | c_void_p, "funcN", 309 | c_void_p, "*func_argN", 310 | c_void_p, "butm_func", 311 | c_void_p, "*butm_func_arg", 312 | c_void_p, "handle_func", 313 | c_void_p, "*handle_func_arg", 314 | c_void_p, "*block_event_func", 315 | c_void_p, "*drawextra", 316 | c_void_p, "*drawextra_arg1", 317 | c_void_p, "*drawextra_arg2", 318 | c_int, "flag", 319 | # c_short, "alignnr", 320 | # c_short, "content_hints", 321 | # c_char, "direction", 322 | # c_char, "theme_style", 323 | # c_char, "dt", 324 | ) 325 | 326 | uiBut._fields_ = gen_fields( 327 | uiBut, "*next", "*prev", 328 | c_int, "flag", "drawflag", 329 | c_int, "type", 330 | c_int, "pointype", 331 | c_short, "bit", "bitnr", "retval", "strwidth", "alignnr", 332 | c_short, "ofs", "pos", "selsta", "selend", 333 | c_char, "*str", 334 | c_char * UI_MAX_NAME_STR, "strdata", 335 | c_char * UI_MAX_DRAW_STR, "drawstr", 336 | rctf, "rect", 337 | ) 338 | 339 | bContext_wm._fields_ = gen_fields( 340 | c_void_p, "*manager", 341 | c_void_p, "*window", 342 | (True, (2, 80, 0), c_void_p, "*workspace"), 343 | c_void_p, "*screen", 344 | ScrArea, "*area", 345 | ARegion, "*region", 346 | c_void_p, "*menu", 347 | (True, (2, 80, 0), c_void_p, "*gizmo_group"), 348 | c_void_p, "*store", 349 | c_char_p, "*operator_poll_msg", 350 | ) 351 | 352 | bContext_data._fields_ = gen_fields( 353 | c_void_p, "*main", 354 | c_void_p, "*scene", 355 | c_int, "recursion", 356 | c_int, "py_init", 357 | c_void_p, "py_context", 358 | ) 359 | 360 | bContext._fields_ = gen_fields( 361 | c_int, "thread", 362 | bContext_wm, "wm", 363 | bContext_data, "data", 364 | ) 365 | 366 | vec2s._fields_ = gen_fields( 367 | c_short, "x", "y" 368 | ) 369 | 370 | ScrVert._fields_ = gen_fields( 371 | ScrVert, "*next", "*prev", "*newv", 372 | vec2s, "vec" 373 | ) 374 | 375 | # source/blender/makesdna/DNA_screen_types.h 376 | ScrArea._fields_ = gen_fields( 377 | ScrArea, "*next", "*prev", 378 | ScrVert, "*v1", "*v2", "*v3", "*v4", 379 | c_void_p, "*full", 380 | rcti, "totrct", 381 | c_char, "spacetype", "butspacetype", 382 | (True, (2, 80, 0), c_short, "butspacetype_subtype"), 383 | c_short, "winx", "winy", 384 | (True, (2, 80, 0), c_char, "headertype"), 385 | (False, (2, 80, 0), c_short, "headertype"), 386 | (True, (2, 80, 0), c_char, "do_refresh"), 387 | (False, (2, 80, 0), c_short, "do_refresh"), 388 | c_short, "flag", 389 | c_short, "region_active_win", 390 | c_char, "temp", "pad", 391 | c_void_p, "*type", 392 | (True, (2, 80, 0), c_void_p, "*global"), 393 | ListBase, "spacedata", 394 | ) 395 | 396 | # source/blender/makesdna/DNA_screen_types.h 397 | ScrAreaMap._fields_ = gen_fields( 398 | ListBase, "vertbase", 399 | ListBase, "edgebase", 400 | ListBase, "areabase", 401 | ) 402 | 403 | # source/blender/makesdna/DNA_screen_types.h 404 | bScreen._fields_ = gen_fields( 405 | ID, "id", 406 | ListBase, "vertbase", 407 | ListBase, "edgebase", 408 | ListBase, "areabase", 409 | ListBase, "regionbase", 410 | c_void_p, "*scene", 411 | (False, (2, 80, 0), c_void_p, "*newscene"), 412 | (True, (2, 80, 0), c_short, "flag"), 413 | c_short, "winid", 414 | c_short, "redraws_flag", 415 | c_char, "temp", 416 | ) 417 | 418 | ''' 419 | wmEvent._fields_ = gen_fields( 420 | wmEvent, "*next", "*prev", 421 | c_short, "type", 422 | c_short, "val", 423 | c_int, "x", "y", 424 | c_int, "mval[2]", 425 | c_char, "utf8_buf[6]", 426 | c_char, "ascii", 427 | c_char, "pad", 428 | c_short, "prevtype", 429 | c_short, "prevval", 430 | c_int, "prevx", "prevy", 431 | c_double, "prevclicktime", 432 | c_int, "prevclickx", "prevclicky", 433 | c_short, "shift", "ctrl", "alt", "oskey", 434 | c_short, "keymodifier", 435 | ) 436 | ''' 437 | 438 | wmWindow._fields_ = gen_fields( 439 | wmWindow, "*next", "*prev", 440 | c_void_p, "*ghostwin", 441 | (True, (2, 80, 0), c_void_p, "*gpuctx"), 442 | (True, (2, 80, 0), wmWindow, "*parent"), 443 | (False, (2, 80, 0), bScreen, "*screen"), 444 | (False, (2, 80, 0), bScreen, "*newscreen"), 445 | (True, (2, 80, 0), c_void_p, "*scene"), 446 | (True, (2, 80, 0), c_void_p, "*new_scene"), 447 | (True, (2, 80, 0), c_char, "view_layer_name[64]"), 448 | (False, (2, 80, 0), c_char, "screenname[64]"), 449 | (True, (2, 80, 0), c_void_p, "*workspace_hook"), 450 | (True, (2, 80, 0), ScrAreaMap, "global_areas"), 451 | (True, (2, 80, 0), bScreen, "*screen"), 452 | c_short, "posx", "posy", "sizex", "sizey", 453 | c_short, "windowstate", 454 | c_short, "monitor", 455 | c_short, "active", 456 | c_short, "cursor", 457 | c_short, "lastcursor", 458 | c_short, "modalcursor", 459 | c_short, "grabcursor", 460 | c_short, "addmousemove", 461 | (False, (2, 80, 0), c_short, "multisamples"), 462 | (False, (2, 80, 0), c_short, "pad[3]"), 463 | (True, (2, 80, 0), c_short, "pad[4]"), 464 | c_int, "winid", 465 | c_short, "lock_pie_event", 466 | c_short, "last_pie_event", 467 | c_void_p, "*eventstate", 468 | (False, (2, 80, 0), c_void_p, "*curswin"), 469 | c_void_p, "*tweak", 470 | c_void_p, "*ime_data", 471 | (False, (2, 80, 0), c_int, "drawmethod", "drawfail"), 472 | (False, (2, 80, 0), ListBase, "drawdata"), 473 | ListBase, "queue", 474 | ListBase, "handlers", 475 | ListBase, "modalhandlers", 476 | ) 477 | 478 | wmEventHandler_KeymapFn._fields_ = gen_fields( 479 | c_void_p, "*handle_post_fn", 480 | c_void_p, "*user_data" 481 | ) 482 | 483 | wmEventHandler._fields_ = gen_fields( 484 | wmEventHandler, "*next", "*prev", 485 | c_char, "type", 486 | c_char, "flag", 487 | c_void_p, "*keymap", 488 | c_void_p, "*bblocal", "*bbwin", 489 | (True, (2, 80, 0), wmEventHandler_KeymapFn, "keymap_callback"), 490 | (True, (2, 80, 0), c_void_p, "*keymap_tool"), 491 | wmOperator, "*op", 492 | ) 493 | 494 | wmOperator._fields_ = gen_fields( 495 | wmOperator, "*next", "*prev", 496 | c_char, "idname[64]", 497 | ) 498 | 499 | del re_field 500 | del struct 501 | del gen_fields 502 | 503 | 504 | class HeadModalHandler: 505 | key: bpy.props.StringProperty( 506 | default="ESC", options={'SKIP_SAVE'}) 507 | 508 | def __init__(self): 509 | self.move_flag = False 510 | self.finished = False 511 | 512 | def finish(self): 513 | pass 514 | 515 | def modal(self, context, event): 516 | if event.value == 'RELEASE': 517 | if event.type == self.key: 518 | self.finished = True 519 | return {'PASS_THROUGH'} 520 | 521 | if self.move_flag: 522 | self.move_flag = False 523 | if not move_modal_handler(context.window, self): 524 | self.finished = True 525 | 526 | if event.type != 'TIMER': 527 | self.move_flag = True 528 | elif self.finished: 529 | context.window_manager.event_timer_remove(self.timer) 530 | self.timer = None 531 | self.finish() 532 | return {'FINISHED'} 533 | 534 | return {'PASS_THROUGH'} 535 | 536 | def execute(self, context): 537 | self.timer = context.window_manager.event_timer_add( 538 | 0.001, window=context.window) 539 | context.window_manager.modal_handler_add(self) 540 | return {'RUNNING_MODAL'} 541 | 542 | def invoke(self, context, event): 543 | return self.execute(context) 544 | 545 | 546 | def c_layout(layout): 547 | ret = cast(layout.as_pointer(), POINTER(uiLayout)).contents 548 | return ret 549 | 550 | 551 | def c_last_btn(clayout): 552 | ret = cast( 553 | clayout.root.contents.block.contents.buttons.last, 554 | POINTER(uiBut)).contents 555 | return ret 556 | 557 | 558 | def c_style(clayout): 559 | return clayout.root.contents.style.contents 560 | 561 | 562 | def c_context(context): 563 | ret = cast(context.as_pointer(), POINTER(bContext)).contents 564 | return ret 565 | 566 | 567 | # def c_event(event): 568 | # ret = cast(event.as_pointer(), POINTER(wmEvent)).contents 569 | # return ret 570 | 571 | 572 | def c_window(v): 573 | return cast(v.as_pointer(), POINTER(wmWindow)).contents 574 | 575 | 576 | def c_handler(v): 577 | return cast(v, POINTER(wmEventHandler)).contents 578 | 579 | 580 | def c_operator(v): 581 | return cast(v, POINTER(wmOperator)).contents 582 | 583 | 584 | def c_area(v): 585 | return cast(v.as_pointer(), POINTER(ScrArea)).contents 586 | 587 | 588 | def set_area(context, area=None): 589 | C = c_context(context) 590 | if area: 591 | set_area.area = C.wm.area 592 | C.wm.area = cast( 593 | area.as_pointer(), POINTER(ScrArea)) 594 | 595 | elif hasattr(set_area, "area"): 596 | C.wm.area = set_area.area 597 | 598 | 599 | def set_region(context, region=None): 600 | C = c_context(context) 601 | if region: 602 | set_region.region = C.wm.region 603 | C.wm.region = cast( 604 | region.as_pointer(), POINTER(ARegion)) 605 | 606 | elif hasattr(set_region, "region"): 607 | C.wm.region = set_region.region 608 | 609 | 610 | def area_rect(area): 611 | carea = cast(area.as_pointer(), POINTER(ScrArea)) 612 | return carea.contents.totrct 613 | 614 | 615 | def set_temp_screen(screen): 616 | cscreen = cast(screen.as_pointer(), POINTER(bScreen)) 617 | cscreen.contents.temp = 1 618 | 619 | 620 | def is_row(layout): 621 | clayout = cast(layout.as_pointer(), POINTER(uiLayout)) 622 | croot = cast(clayout, POINTER(uiLayoutRoot)) 623 | return croot.contents.type == 1 624 | 625 | 626 | def swap_spaces(from_area, to_area, to_area_space_type): 627 | idx = -1 628 | for i, s in enumerate(to_area.spaces): 629 | if s.type == to_area_space_type: 630 | idx = i 631 | break 632 | else: 633 | return 634 | 635 | from_area_p = c_area(from_area) 636 | to_area_p = c_area(to_area) 637 | 638 | from_space_a = from_area_p.spacedata.first 639 | to_space_p = to_area_p.spacedata.find(idx) 640 | to_space_a = addressof(to_space_p) 641 | to_prev_space_a = addressof(to_space_p.prev.contents) 642 | 643 | from_area_p.spacedata.remove(from_space_a) 644 | to_area_p.spacedata.remove(to_space_a) 645 | 646 | from_area_p.spacedata.insert(None, to_space_a) 647 | to_area_p.spacedata.insert(to_prev_space_a, from_space_a) 648 | 649 | 650 | def resize_area(area, width, direction='RIGHT'): 651 | area_p = c_area(area) 652 | dx = width - area.width 653 | if direction == 'LEFT': 654 | area_p.v1.contents.vec.x -= dx 655 | area_p.v2.contents.vec.x -= dx 656 | elif direction == 'RIGHT': 657 | area_p.v3.contents.vec.x += dx 658 | area_p.v4.contents.vec.x += dx 659 | 660 | 661 | def move_modal_handler(window, operator): 662 | a_operator = operator.as_pointer() 663 | w = cast(window.as_pointer(), POINTER(wmWindow)).contents 664 | p_eh = POINTER(wmEventHandler) 665 | p_op = POINTER(wmOperator) 666 | p_h_first = cast(w.modalhandlers.first, p_eh) 667 | 668 | if not p_h_first: 669 | return False 670 | 671 | h_first = h = p_h_first.contents 672 | 673 | p_o = cast(h.op, p_op) 674 | if p_o: 675 | o = p_o.contents 676 | if addressof(o) == a_operator: 677 | return True 678 | 679 | while h: 680 | p_o = cast(h.op, p_op) 681 | if p_o: 682 | o = p_o.contents 683 | if addressof(o) == a_operator: 684 | p_h_prev = cast(h.prev, p_eh) 685 | p_h_next = cast(h.next, p_eh) 686 | if p_h_prev: 687 | p_h_prev.contents.next = p_h_next 688 | if p_h_next: 689 | p_h_next.contents.prev = p_h_prev 690 | h.prev = None 691 | h.next = p_h_first 692 | w.modalhandlers.first = addressof(h) 693 | h_first.prev = cast(w.modalhandlers.first, p_eh) 694 | return True 695 | 696 | h = cast(h.next, p_eh) 697 | h = h and h.contents 698 | 699 | return False 700 | 701 | 702 | def keep_pie_open(layout): 703 | layout_p = c_layout(layout) 704 | block_p = layout_p.root.contents.block.contents 705 | block_p.flag |= UI_BLOCK_KEEP_OPEN 706 | # block_p.dt = UI_EMBOSS 707 | 708 | 709 | def register(): 710 | pme.context.add_global("keep_pie_open", keep_pie_open) 711 | -------------------------------------------------------------------------------- /collection_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import pme 3 | from .layout_helper import lh 4 | from .bl_utils import ConfirmBoxHandler 5 | 6 | 7 | def sort_collection(collection, key, data=None, idx_prop=None): 8 | cur_name = None 9 | if data and idx_prop is not None: 10 | cur_name = collection[getattr(data, idx_prop)].name 11 | 12 | items = [item for item in collection] 13 | items.sort(key=key) 14 | items = [item.name for item in items] 15 | 16 | idx = len(items) - 1 17 | while idx > 0: 18 | name = items[idx] 19 | if collection[idx] != collection[name]: 20 | idx1 = collection.find(name) 21 | collection.move(idx1, idx) 22 | idx -= 1 23 | 24 | if cur_name: 25 | setattr(data, idx_prop, collection.find(cur_name)) 26 | 27 | 28 | def move_item(collection, old_idx, new_idx, indices=None): 29 | collection.move(old_idx, new_idx) 30 | 31 | if indices: 32 | n = len(indices) 33 | for i in range(n): 34 | if indices[i] == old_idx: 35 | indices[i] = new_idx 36 | elif old_idx < indices[i] <= new_idx: 37 | indices[i] -= 1 38 | elif new_idx <= indices[i] < old_idx: 39 | indices[i] += 1 40 | return indices[0] if n == 1 else indices 41 | 42 | return None 43 | 44 | 45 | def remove_item(collection, idx, indices=None): 46 | collection.remove(idx) 47 | 48 | if indices: 49 | n = len(indices) 50 | for i in range(n): 51 | if indices[i] > idx: 52 | indices[i] -= 1 53 | return indices[0] if n == 1 else indices 54 | 55 | return None 56 | 57 | 58 | def find_by(collection, key, value): 59 | for item in collection: 60 | item_value = getattr(item, key, None) 61 | if item_value and item_value == value: 62 | return item 63 | 64 | return None 65 | 66 | 67 | class AddItemOperator: 68 | bl_label = "Add Item" 69 | bl_description = "Add an item" 70 | bl_options = {'INTERNAL'} 71 | 72 | idx: bpy.props.IntProperty(default=-1, options={'SKIP_SAVE'}) 73 | 74 | def get_collection(self): 75 | return None 76 | 77 | def finish(self, item): 78 | pass 79 | 80 | def execute(self, context): 81 | collection = self.get_collection() 82 | item = collection.add() 83 | 84 | idx = len(collection) - 1 85 | if 0 <= self.idx < idx: 86 | collection.move(idx, self.idx) 87 | item = collection[self.idx] 88 | 89 | self.finish(item) 90 | return {'FINISHED'} 91 | 92 | 93 | class MoveItemOperator: 94 | label_prop = "name" 95 | bl_idname = None 96 | bl_label = "Move Item" 97 | bl_description = "Move the item" 98 | bl_options = {'INTERNAL'} 99 | 100 | old_idx: bpy.props.IntProperty(default=-1, options={'SKIP_SAVE'}) 101 | old_idx_last: bpy.props.IntProperty(default=-1, options={'SKIP_SAVE'}) 102 | new_idx: bpy.props.IntProperty(default=-1, options={'SKIP_SAVE'}) 103 | swap: bpy.props.BoolProperty(options={'SKIP_SAVE'}) 104 | 105 | def get_collection(self): 106 | return None 107 | 108 | def get_icon(self, item, idx): 109 | return 'KEYTYPE_KEYFRAME_VEC' if idx == self.old_idx else 'HANDLETYPE_FREE_VEC' 110 | 111 | def get_title(self): 112 | return "Move Item" 113 | 114 | def get_title_icon(self): 115 | return 'ARROW_LEFTRIGHT' if self.swap else 'FORWARD' 116 | 117 | def filter_item(self, item, idx): 118 | return True 119 | 120 | def draw_menu(self, menu, context): 121 | lh.lt(menu.layout) 122 | collection = self.get_collection() 123 | 124 | lh.label(self.get_title(), self.get_title_icon()) 125 | lh.sep() 126 | 127 | for i, item in enumerate(collection): 128 | if not self.filter_item(item, i): 129 | continue 130 | 131 | name = getattr(item, self.label_prop, None) or "..." 132 | icon = self.get_icon(item, i) 133 | 134 | lh.operator( 135 | self.bl_idname, name, icon, 136 | old_idx=self.old_idx, 137 | old_idx_last=self.old_idx_last, 138 | new_idx=i, 139 | swap=self.swap 140 | ) 141 | 142 | def finish(self): 143 | pass 144 | 145 | def execute(self, context): 146 | collection = self.get_collection() 147 | if self.old_idx < 0 or self.old_idx >= len(collection): 148 | return {'CANCELLED'} 149 | 150 | if self.old_idx_last >= 0 and ( 151 | self.old_idx_last >= len(collection) or 152 | self.old_idx_last < self.old_idx): 153 | return {'CANCELLED'} 154 | 155 | if self.new_idx == -1: 156 | bpy.context.window_manager.popup_menu(self.draw_menu) 157 | return {'FINISHED'} 158 | 159 | if self.new_idx < 0 or self.new_idx >= len(collection): 160 | return {'CANCELLED'} 161 | 162 | if self.new_idx != self.old_idx: 163 | if self.old_idx_last < 0: 164 | collection.move(self.old_idx, self.new_idx) 165 | 166 | if self.swap: 167 | swap_idx = self.new_idx - 1 \ 168 | if self.old_idx < self.new_idx \ 169 | else self.new_idx + 1 170 | if swap_idx != self.old_idx: 171 | collection.move(swap_idx, self.old_idx) 172 | 173 | else: 174 | if self.new_idx < self.old_idx: 175 | for i in range(self.old_idx, self.old_idx_last + 1): 176 | collection.move(self.old_idx_last, self.new_idx) 177 | else: 178 | for i in range(0, self.old_idx_last - self.old_idx + 1): 179 | collection.move( 180 | self.old_idx_last - i, self.new_idx - i) 181 | 182 | self.finish() 183 | 184 | return {'FINISHED'} 185 | 186 | 187 | class RemoveItemOperator(ConfirmBoxHandler): 188 | bl_label = "Remove Item" 189 | bl_description = "Remove the item" 190 | bl_options = {'INTERNAL'} 191 | 192 | idx: bpy.props.IntProperty(options={'SKIP_SAVE'}) 193 | 194 | def get_collection(self): 195 | return None 196 | 197 | def finish(self): 198 | pass 199 | 200 | def on_confirm(self, value): 201 | if not value: 202 | return 203 | 204 | collection = self.get_collection() 205 | if self.idx < 0 or self.idx >= len(collection): 206 | return 207 | 208 | collection.remove(self.idx) 209 | 210 | self.finish() 211 | 212 | 213 | class BaseCollectionItem(bpy.types.PropertyGroup): 214 | pass 215 | 216 | 217 | def register(): 218 | pme.context.add_global("find_by", find_by) 219 | -------------------------------------------------------------------------------- /compatibility_fixes.py: -------------------------------------------------------------------------------- 1 | import re 2 | from . import addon 3 | from .addon import prefs 4 | from .debug_utils import * 5 | from . import constants as CC 6 | 7 | 8 | def fix(pms=None, version=None): 9 | DBG_INIT and logh("PME Fixes") 10 | pr = prefs() 11 | pr_version = version or tuple(pr.version) 12 | if pr_version == addon.VERSION: 13 | return 14 | 15 | fixes = [] 16 | re_fix = re.compile(r"fix_(\d+)_(\d+)_(\d+)") 17 | for k, v in globals().items(): 18 | mo = re_fix.search(k) 19 | if not mo: 20 | continue 21 | 22 | fix_version = (int(mo.group(1)), int(mo.group(2)), int(mo.group(3))) 23 | if fix_version <= pr_version or fix_version > addon.VERSION: 24 | continue 25 | fixes.append((fix_version, v)) 26 | 27 | fixes.sort(key=lambda item: item[0]) 28 | 29 | if pms is None: 30 | pms = pr.pie_menus 31 | 32 | for pm in pms: 33 | for fix_version, fix_func in fixes: 34 | fix_func(pr, pm) 35 | 36 | pr.version = addon.VERSION 37 | 38 | 39 | def fix_json(pm, menu, version): 40 | DBG_INIT and logh("PME JSON Fixes") 41 | pr = prefs() 42 | fixes = [] 43 | re_fix = re.compile(r"fix_json_(\d+)_(\d+)_(\d+)") 44 | for k, v in globals().items(): 45 | mo = re_fix.search(k) 46 | if not mo: 47 | continue 48 | 49 | fix_version = (int(mo.group(1)), int(mo.group(2)), int(mo.group(3))) 50 | if fix_version <= version: 51 | continue 52 | fixes.append((fix_version, v)) 53 | 54 | fixes.sort(key=lambda item: item[0]) 55 | 56 | for fix_version, fix_func in fixes: 57 | fix_func(pr, pm, menu) 58 | 59 | 60 | def fix_1_14_0(pr, pm): 61 | if pm.mode == 'PMENU': 62 | for pmi in pm.pmis: 63 | if pmi.mode == 'MENU': 64 | sub_pm = pmi.text in pr.pie_menus and \ 65 | pr.pie_menus[pmi.text] 66 | 67 | if sub_pm and sub_pm.mode == 'DIALOG' and \ 68 | sub_pm.get_data("pd_panel") == 0: 69 | pmi.text = CC.F_EXPAND + pmi.text 70 | 71 | if sub_pm.get_data("pd_box"): 72 | pmi.text = CC.F_EXPAND + pmi.text 73 | 74 | elif pm.mode == 'DIALOG': 75 | if pm.get_data("pd_expand"): 76 | pm.set_data("pd_expand", False) 77 | for pmi in pm.pmis: 78 | if pmi.mode == 'MENU': 79 | sub_pm = pmi.text in pr.pie_menus and \ 80 | pr.pie_menus[pmi.text] 81 | if sub_pm and sub_pm.mode == 'DIALOG': 82 | pmi.text = CC.F_EXPAND + pmi.text 83 | 84 | 85 | def fix_1_14_9(pr, pm): 86 | if pm.mode == 'STICKY': 87 | pm.data = re.sub(r"([^_])block_ui", r"\1sk_block_ui", pm.data) 88 | 89 | 90 | def fix_1_17_0(pr, pm): 91 | if pm.mode == 'PMENU': 92 | for i in range(len(pm.pmis), 10): 93 | pm.pmis.add() 94 | 95 | 96 | def fix_1_17_1(pr, pm): 97 | if not pm.ed.has_hotkey: 98 | return 99 | 100 | pm.km_name = (CC.KEYMAP_SPLITTER + " ").join(pm.km_name.split(",")) 101 | 102 | 103 | def fix_json_1_17_1(pr, pm, menu): 104 | if not pm.ed.has_hotkey: 105 | return 106 | 107 | menu[1] = (CC.KEYMAP_SPLITTER + " ").join(menu[1].split(",")) 108 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .addon import ic, is_28 3 | from .previews_helper import ph 4 | 5 | MAX_STR_LEN = 1024 6 | UNTAGGED = "Untagged" 7 | TREE_SPLITTER = '→' 8 | KEYMAP_SPLITTER = ';' 9 | TREE_ROOT = "root" 10 | F_ICON_ONLY = "#" 11 | F_HIDDEN = "!" 12 | F_CB = "^" 13 | F_EXPAND = "@" 14 | F_CUSTOM_ICON = "@" 15 | F_RIGHT = "_right" 16 | F_PRE = "_pre" 17 | PMIF_DISABLED = 1 18 | PANEL_FILE = "sub" 19 | PANEL_FOLDER = "" 20 | BL_TIMER_STEP = 0.01 21 | 22 | PME_TEMP_SCREEN = "PME Temp " 23 | PME_SCREEN = "PME " 24 | 25 | POPUP_PADDING = 10 26 | WINDOW_MARGIN = 32 27 | WINDOW_MIN_WIDTH = 320 28 | WINDOW_MIN_HEIGHT = 240 29 | 30 | UPREFS = 'USER_PREFERENCES' 31 | UPREFS_CLS = "UserPreferences" 32 | UPREFS_ID = "user_preferences" 33 | if 'USER_PREFERENCES' not in bpy.types.Area.bl_rna.properties[ 34 | 'type'].enum_items: 35 | UPREFS = 'PREFERENCES' 36 | UPREFS_CLS = "Preferences" 37 | UPREFS_ID = "preferences" 38 | 39 | ED_DATA = ( 40 | ('PMENU', "Pie Menu", 'MOD_SUBSURF'), 41 | ('RMENU', "Regular Menu", 'MOD_BOOLEAN'), 42 | ('DIALOG', "Popup Dialog", 'MOD_BUILD'), 43 | ('SCRIPT', "Stack Key", 'MOD_MIRROR'), 44 | ('PANEL', "Panel Group", 'MOD_MULTIRES'), 45 | ('HPANEL', "Hidden Panel Group", 'MOD_TRIANGULATE'), 46 | ('STICKY', "Sticky Key", 'MOD_WARP'), 47 | ('MACRO', "Macro Operator", 'MOD_ARRAY'), 48 | ('MODAL', "Modal Operator", 'MOD_BEVEL'), 49 | ('PROPERTY', "Property", 'MOD_SCREW'), 50 | ) 51 | 52 | EMODE_ITEMS = [ 53 | ('COMMAND', "Command", 54 | "Python code that will be executed when the user clicks the button"), 55 | ('PROP', "Property", 56 | "Path to the object's property which will be displayed as a widget"), 57 | ('MENU', "Menu", 58 | "Open/execute the menu, popup or operator\n" 59 | " when the user clicks the button\n" 60 | "Or draw a popup dialog inside the current popup dialog or pie menu"), 61 | ('HOTKEY', "Hotkey", 62 | "Blender's hotkey that will be used " 63 | "to find and execute the operator assigned to it\n" 64 | " when the user clicks the button"), 65 | ('CUSTOM', "Custom", 66 | "Python code that will be used to draw custom layout of widgets"), 67 | ('INVOKE', "On Invoke", 68 | "Python code that will be executed\n" 69 | " when the user invokes the modal operator"), 70 | ('FINISH', "On Confirm", 71 | "Python code that will be executed\n" 72 | " when the user confirms the modal operator"), 73 | ('CANCEL', "On Cancel", 74 | "Python code that will be executed\n" 75 | " when the user cancels the modal operator"), 76 | ('UPDATE', "On Update", 77 | "Python code that will be executed\n" 78 | " when the user interacts with the modal operator"), 79 | ] 80 | MODE_ITEMS = [ 81 | ('EMPTY', "Empty", "Don't use the item") 82 | ] 83 | MODE_ITEMS.extend(EMODE_ITEMS) 84 | 85 | PD_MODE_ITEMS = ( 86 | ('PIE', 'Pie Mode', ""), 87 | ('PANEL', 'Dialog Mode', ""), 88 | ('POPUP', 'Popup Mode', ""), 89 | ) 90 | 91 | MODAL_CMD_MODES = { 92 | EMODE_ITEMS[0][0], 93 | EMODE_ITEMS[5][0], 94 | EMODE_ITEMS[6][0], 95 | EMODE_ITEMS[7][0], 96 | EMODE_ITEMS[8][0], 97 | } 98 | 99 | PM_ITEMS = tuple( 100 | (id, name, "", icon, i) 101 | for i, (id, name, icon) in enumerate(ED_DATA) 102 | ) 103 | 104 | PM_ITEMS_M = tuple( 105 | (id, name, "", icon, 1 << i) 106 | for i, (id, name, icon) in enumerate(ED_DATA) 107 | ) 108 | 109 | PM_ITEMS_M_DEFAULT = set(id for id, name, icon in ED_DATA) 110 | 111 | SETTINGS_TAB_ITEMS = ( 112 | ('GENERAL', "General", ""), 113 | ('HOTKEYS', "Hotkeys", ""), 114 | ('OVERLAY', "Overlay", ""), 115 | ('PIE', "Pie Menu", ""), 116 | ('MENU', "Regular Menu", ""), 117 | ('POPUP', "Popup Dialog", ""), 118 | ('MODAL', "Modal Operator", ""), 119 | ) 120 | 121 | SETTINGS_TAB_DEFAULT = SETTINGS_TAB_ITEMS[0][0] 122 | # SETTINGS_TAB_DEFAULT = set(id for id, name, icon in SETTINGS_TAB_ITEMS) 123 | 124 | OP_CTX_ITEMS = ( 125 | ('INVOKE_DEFAULT', "Invoke (Default)", "", 'OUTLINER_OB_LAMP', 0), 126 | ('INVOKE_REGION_WIN', "Invoke Window Region", "", 'OUTLINER_OB_LAMP', 1), 127 | ('INVOKE_REGION_CHANNELS', "Invoke Channels Region", "", 128 | 'OUTLINER_OB_LAMP', 2), 129 | ('INVOKE_REGION_PREVIEW', "Invoke Preview Region", "", 130 | 'OUTLINER_OB_LAMP', 3), 131 | ('INVOKE_AREA', "Invoke Area", "", 'OUTLINER_OB_LAMP', 4), 132 | ('INVOKE_SCREEN', "Invoke Screen", "", 'OUTLINER_OB_LAMP', 5), 133 | ('EXEC_DEFAULT', "Exec", "", 'LAMP_DATA', 6), 134 | ('EXEC_REGION_WIN', "Exec Window Region", "", 'LAMP_DATA', 7), 135 | ('EXEC_REGION_CHANNELS', "Exec Channels Region", "", 'LAMP_DATA', 8), 136 | ('EXEC_REGION_PREVIEW', "Exec Preview Region", "", 'LAMP_DATA', 9), 137 | ('EXEC_AREA', "Exec Area", "", 'LAMP_DATA', 10), 138 | ('EXEC_SCREEN', "Exec Screen", "", 'LAMP_DATA', 11), 139 | ) 140 | 141 | ICON_ON = 'CHECKBOX_HLT' 142 | ICON_OFF = 'CHECKBOX_DEHLT' 143 | 144 | BL_ICONS = { 145 | item.identifier 146 | for item in bpy.types.Property.bl_rna.properties['icon'].enum_items 147 | } 148 | 149 | DEFAULT_POLL = "return True" 150 | 151 | LIST_PADDING = 0.5 152 | SCALE_X = 1.5 153 | SEPARATOR_SCALE_Y = 11 / 18 154 | SPACER_SCALE_Y = 0.3 155 | 156 | I_CLIPBOARD = "Clipboard is empty" 157 | I_CMD = "Bad command" 158 | I_DEBUG = "Debug mode: %s" 159 | I_NO_ERRORS = "No errors were found" 160 | I_MODAL_PROP_MOVE = "Mouse Move mode blocks all Command and Property hotkeys" 161 | W_CMD = "PME: Bad command: %s" 162 | W_FILE = "PME: Bad file" 163 | W_JSON = "PME: Bad json" 164 | W_KEY = "PME: Bad key: %s" 165 | W_PM = "Menu '%s' was not found" 166 | W_PROP = "PME: Bad property: %s" 167 | W_PMI_HOTKEY = "Hotkey is not specified" 168 | W_PMI_EXPR = "Invalid expression" 169 | W_PMI_SYNTAX = "Invalid syntax" 170 | W_PMI_MENU = "Select the item" 171 | W_PMI_ADD_BTN = "Can't add this button" 172 | W_PMI_LONG_CMD = "The command is too long" 173 | 174 | 175 | ARROW_ICONS = ( 176 | "@p4", "@p6", "@p2", "@p8", "@p7", "@p9", "@p1", "@p3", "@pA", "@pB") 177 | 178 | SPACE_ITEMS = ( 179 | ('VIEW_3D', "3D Viewport", "", 'VIEW3D', 0), 180 | ('DOPESHEET_EDITOR', "Dope Sheet", "", 'ACTION', 1), 181 | ('FILE_BROWSER', "File Browser", "", 'FILEBROWSER', 2), 182 | ('GRAPH_EDITOR', "Graph Editor/Drivers", "", 'GRAPH', 3), 183 | ('INFO', "Info", "", 'INFO', 4), 184 | ('LOGIC_EDITOR', "Logic Editor", "", 'ERROR', 5), 185 | ('CLIP_EDITOR', "Movie Clip Editor", "", 'TRACKER', 6), 186 | ('NLA_EDITOR', "NLA Editor", "", 'NLA', 7), 187 | ('NODE_EDITOR', "Node Editor", "", 'NODETREE', 8), 188 | ('OUTLINER', "Outliner", "", 'OUTLINER', 9), 189 | ('PROPERTIES', "Properties", "", 'PROPERTIES', 10), 190 | ('CONSOLE', "Python Console", "", 'CONSOLE', 11), 191 | ('TEXT_EDITOR', "Text Editor", "", 'TEXT', 12), 192 | ('TIMELINE', "Timeline", "", 'TIME', 13), 193 | (UPREFS, "User Preferences", "", 'PREFERENCES', 14), 194 | ('IMAGE_EDITOR', "Image/UV Editor", "", 'IMAGE', 15), 195 | ('SEQUENCE_EDITOR', "Video Sequencer", "", 'SEQUENCE', 16), 196 | ('SPREADSHEET', "Spreadsheet", "", 'SPREADSHEET', 17), 197 | ('TOPBAR', "Top Bar", "", 'TRIA_UP_BAR', 18), 198 | ('STATUSBAR', "Status Bar", "", 'TRIA_DOWN_BAR', 19), 199 | ) 200 | 201 | REGION_ITEMS = ( 202 | ('TOOLS', "Tools (Side Panel)", "T-panel", 'TRIA_LEFT_BAR', 0), 203 | ('UI', "UI (Side Panel)", "N-panel", 'TRIA_RIGHT_BAR', 1), 204 | ('WINDOW', "Window", "Center Area", 'MESH_PLANE', 2), 205 | ('HEADER', "Header", "Top or bottom bar", 'TRIA_DOWN_BAR', 3), 206 | ) 207 | 208 | OPEN_MODE_ITEMS = ( 209 | ('PRESS', "Press", "Press the key", ph.get_icon("pPress"), 0), 210 | ('HOLD', "Hold", "Hold down the key", ph.get_icon("pHold"), 1), 211 | ('DOUBLE_CLICK', "Double Click", "Double click the key", 212 | ph.get_icon("pDouble"), 2), 213 | ('TWEAK', "Click Drag", "Hold down the key and move the mouse", 214 | ph.get_icon("pTweak"), 3), 215 | ('CHORDS', "Key Chords", "Click sequence of 2 keys", 216 | ph.get_icon("pChord"), 4), 217 | ) 218 | 219 | 220 | def header_action_enum_items(): 221 | yield ('DEFAULT', "Default", "", '', 0) 222 | yield ('TOP', "Top", "", '', 1) 223 | yield ('BOTTOM', "Bottom", "", '', 2) 224 | yield ('TOP_HIDE', "Top Hidden", "", '', 3) 225 | yield ('BOTTOM_HIDE', "Bottom Hidden", "", '', 4) 226 | 227 | 228 | class EnumItems(): 229 | def __init__(self): 230 | self._items = [] 231 | 232 | def add_item(self, id, name, icon, desc=""): 233 | self._items.append( 234 | (id, name, desc, ic(icon), len(self._items))) 235 | 236 | def retrieve_items(self): 237 | if self._items is None: 238 | raise ValueError("Items are already retrieved") 239 | 240 | ret = self._items 241 | self._items = None 242 | 243 | return ret 244 | 245 | 246 | def area_type_enum_items(current=True, none=False): 247 | ei = EnumItems() 248 | 249 | if current: 250 | ei.add_item('CURRENT', "Current", 'BLENDER') 251 | 252 | if none: 253 | ei.add_item('NONE', "None", 'HANDLETYPE_FREE_VEC') 254 | 255 | ei.add_item('VIEW_3D', "3D View", 'VIEW3D') 256 | ei.add_item('TIMELINE', "Timeline", 'TIME') 257 | ei.add_item('FCURVES', "Graph Editor", 'GRAPH') 258 | ei.add_item('DRIVERS', "Drivers", 'DRIVER') 259 | ei.add_item('DOPESHEET', "Dope Sheet", 'ACTION') 260 | ei.add_item('NLA_EDITOR', "NLA Editor", 'NLA') 261 | ei.add_item('VIEW', "Image Editor", 'IMAGE') 262 | ei.add_item('UV', "UV Editor", 'UV') 263 | ei.add_item('CLIP_EDITOR', "Movie Clip Editor", 'TRACKER') 264 | ei.add_item('SEQUENCE_EDITOR', "Video Sequence Editor", 'SEQUENCE') 265 | ei.add_item('ShaderNodeTree', "Shader Editor", 'NODE_MATERIAL') 266 | ei.add_item('CompositorNodeTree', "Compositing", 'NODE_COMPOSITING') 267 | ei.add_item('TextureNodeTree', "Texture Node Editor", 'NODE_TEXTURE') 268 | ei.add_item('GeometryNodeTree', "Geometry Node Editor", 'NODETREE') 269 | ei.add_item('TEXT_EDITOR', "Text Editor", 'TEXT') 270 | ei.add_item('PROPERTIES', "Properties", 'PROPERTIES') 271 | ei.add_item('OUTLINER', "Outliner", 'OUTLINER') 272 | ei.add_item(UPREFS, "User Preferences", 'PREFERENCES') 273 | ei.add_item('INFO', "Info", 'INFO') 274 | ei.add_item('FILE_BROWSER', "File Browser", 'FILEBROWSER') 275 | ei.add_item('ASSETS', "Asset Browser", 'ASSET_MANAGER') 276 | ei.add_item('SPREADSHEET', "Spreadsheet", 'SPREADSHEET') 277 | ei.add_item('CONSOLE', "Python Console", 'CONSOLE') 278 | 279 | return ei.retrieve_items() 280 | -------------------------------------------------------------------------------- /debug_utils.py: -------------------------------------------------------------------------------- 1 | DBG = False 2 | DBG_INIT = False 3 | DBG_LAYOUT = False 4 | DBG_TREE = False 5 | DBG_CMD_EDITOR = False 6 | DBG_MACRO = False 7 | DBG_STICKY = False 8 | DBG_STACK = False 9 | DBG_PANEL = False 10 | DBG_PM = False 11 | DBG_PROP = False 12 | DBG_PROP_PATH = False 13 | 14 | 15 | def _log(color, *args): 16 | msg = "" 17 | for arg in args: 18 | if msg: 19 | msg += ", " 20 | msg += str(arg) 21 | print(color + msg + '\033[0m') 22 | 23 | 24 | def logi(*args): 25 | _log('\033[34m', *args) 26 | 27 | 28 | def loge(*args): 29 | _log('\033[31m', *args) 30 | 31 | 32 | def logh(msg): 33 | _log('\033[1;32m', "") 34 | _log('\033[1;32m', msg) 35 | 36 | 37 | def logw(*args): 38 | _log('\033[33m', *args) 39 | -------------------------------------------------------------------------------- /ed_hpanel_group.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .ed_base import EditorBase 3 | from .addon import prefs, temp_prefs, SAFE_MODE 4 | from .layout_helper import lh 5 | from . import panel_utils as PAU 6 | from .ui import tag_redraw 7 | from .operators import * 8 | 9 | 10 | class PME_OT_hpanel_menu(bpy.types.Operator): 11 | bl_idname = "pme.panel_hide_menu" 12 | bl_label = "Hide Panels" 13 | bl_description = "Hide panels" 14 | 15 | def _draw(self, menu, context): 16 | pr = prefs() 17 | lh.lt(menu.layout, 'INVOKE_DEFAULT') 18 | lh.operator( 19 | PME_OT_panel_hide.bl_idname, None, 'ADD', 20 | group=pr.selected_pm.name) 21 | lh.operator(PME_OT_panel_hide_by.bl_idname, None, 'ADD') 22 | lh.sep() 23 | 24 | lh.prop(pr, "interactive_panels") 25 | 26 | def execute(self, context): 27 | context.window_manager.popup_menu( 28 | self._draw, title=self.bl_description) 29 | return {'FINISHED'} 30 | 31 | 32 | class PME_OT_hpanel_remove(bpy.types.Operator): 33 | bl_idname = "pme.hpanel_remove" 34 | bl_label = "Unhide Panel" 35 | bl_description = "Unhide panel" 36 | bl_options = {'INTERNAL'} 37 | 38 | idx: bpy.props.IntProperty() 39 | 40 | def execute(self, context): 41 | pm = prefs().selected_pm 42 | 43 | if self.idx == -1: 44 | PAU.unhide_panels([pmi.text for pmi in pm.pmis]) 45 | 46 | pm.pmis.clear() 47 | 48 | else: 49 | pmi = pm.pmis[self.idx] 50 | PAU.unhide_panel(pmi.text) 51 | pm.pmis.remove(self.idx) 52 | 53 | tag_redraw() 54 | return {'FINISHED'} 55 | 56 | 57 | class Editor(EditorBase): 58 | 59 | def __init__(self): 60 | self.id = 'HPANEL' 61 | EditorBase.__init__(self) 62 | 63 | self.docs = "#Hiding_Unused_Panels" 64 | self.use_preview = False 65 | self.sub_item = False 66 | self.has_hotkey = False 67 | self.has_extra_settings = False 68 | self.default_pmi_data = "hpg?" 69 | self.supported_slot_modes = {'EMPTY'} 70 | 71 | def init_pm(self, pm): 72 | if pm.enabled and not SAFE_MODE: 73 | for pmi in pm.pmis: 74 | PAU.hide_panel(pmi.text) 75 | 76 | def on_pm_remove(self, pm): 77 | for pmi in pm.pmis: 78 | PAU.unhide_panel(pmi.text) 79 | super().on_pm_remove(pm) 80 | 81 | def on_pm_duplicate(self, from_pm, pm): 82 | pass 83 | 84 | def on_pm_enabled(self, pm, value): 85 | super().on_pm_enabled(pm, value) 86 | 87 | if pm.enabled: 88 | for pmi in pm.pmis: 89 | PAU.hide_panel(pmi.text) 90 | 91 | else: 92 | PAU.unhide_panels([pmi.text for pmi in pm.pmis]) 93 | 94 | def draw_keymap(self, layout, data): 95 | pass 96 | 97 | def draw_hotkey(self, layout, data): 98 | pass 99 | 100 | def draw_items(self, layout, pm): 101 | tpr = temp_prefs() 102 | 103 | row = layout.row() 104 | row.template_list( 105 | "WM_UL_panel_list", "", 106 | pm, "pmis", tpr, "hidden_panels_idx", rows=10) 107 | 108 | lh.column(row) 109 | lh.operator(PME_OT_hpanel_menu.bl_idname, "", 'ADD') 110 | 111 | if len(pm.pmis): 112 | lh.operator( 113 | PME_OT_hpanel_remove.bl_idname, "", 'REMOVE', 114 | idx=tpr.hidden_panels_idx) 115 | lh.operator( 116 | PME_OT_hpanel_remove.bl_idname, "", 'X', idx=-1) 117 | 118 | lh.sep() 119 | 120 | lh.layout.prop( 121 | prefs(), "panel_info_visibility", text="", expand=True) 122 | 123 | 124 | def register(): 125 | Editor() 126 | -------------------------------------------------------------------------------- /ed_macro.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import pme 3 | from .addon import prefs 4 | from .constants import MAX_STR_LEN 5 | from .bl_utils import uname 6 | from .ed_base import EditorBase 7 | from .operators import PME_OT_sticky_key_base, PME_OT_modal_base 8 | from . import macro_utils as MAU 9 | 10 | 11 | class PME_OT_macro_exec_base: 12 | bl_idname = "pme.macro_exec_base" 13 | bl_label = "Macro Command" 14 | bl_options = {'INTERNAL'} 15 | 16 | macro_globals = None 17 | 18 | cmd: bpy.props.StringProperty( 19 | maxlen=MAX_STR_LEN, options={'SKIP_SAVE', 'HIDDEN'}) 20 | 21 | def execute(self, context): 22 | if not pme.context.exe( 23 | self.cmd, PME_OT_macro_exec_base.macro_globals): 24 | return {'CANCELLED'} 25 | 26 | ret = {'CANCELLED'} \ 27 | if PME_OT_macro_exec_base.macro_globals.get("stop", False) \ 28 | else {'FINISHED'} 29 | PME_OT_macro_exec_base.macro_globals.pop("stop", None) 30 | return ret 31 | 32 | 33 | class PME_OT_macro_exec1(bpy.types.Operator): 34 | bl_idname = "pme.macro_exec1" 35 | bl_label = "Macro Command" 36 | bl_options = {'INTERNAL'} 37 | 38 | cmd: bpy.props.StringProperty( 39 | maxlen=MAX_STR_LEN, options={'SKIP_SAVE', 'HIDDEN'}) 40 | 41 | def execute(self, context): 42 | PME_OT_macro_exec_base.macro_globals = pme.context.gen_globals() 43 | 44 | return PME_OT_macro_exec_base.execute(self, context) 45 | 46 | 47 | class Editor(EditorBase): 48 | 49 | def __init__(self): 50 | self.id = 'MACRO' 51 | EditorBase.__init__(self) 52 | 53 | self.docs = "#Macro_Operator_Editor" 54 | self.use_slot_icon = False 55 | self.use_preview = False 56 | self.default_pmi_data = "m?" 57 | self.supported_slot_modes = {'COMMAND', 'MENU'} 58 | self.supported_sub_menus = {'STICKY', 'MACRO', 'MODAL'} 59 | 60 | def init_pm(self, pm): 61 | if pm.enabled: 62 | MAU.add_macro(pm) 63 | 64 | def on_pm_add(self, pm): 65 | pmi = pm.pmis.add() 66 | pmi.mode = 'COMMAND' 67 | pmi.name = "Command 1" 68 | MAU.add_macro(pm) 69 | 70 | def on_pm_remove(self, pm): 71 | MAU.remove_macro(pm) 72 | super().on_pm_remove(pm) 73 | 74 | def on_pm_duplicate(self, from_pm, pm): 75 | EditorBase.on_pm_duplicate(self, from_pm, pm) 76 | MAU.add_macro(pm) 77 | 78 | def on_pm_enabled(self, pm, value): 79 | super().on_pm_enabled(pm, value) 80 | 81 | if pm.enabled: 82 | MAU.add_macro(pm) 83 | else: 84 | MAU.remove_macro(pm) 85 | 86 | def on_pm_rename(self, pm, name): 87 | MAU.remove_macro(pm) 88 | super().on_pm_rename(pm, name) 89 | MAU.add_macro(pm) 90 | 91 | def on_pmi_add(self, pm, pmi): 92 | pmi.mode = 'COMMAND' 93 | pmi.name = uname(pm.pmis, "Command", " ", 1, False) 94 | MAU.update_macro(pm) 95 | 96 | def on_pmi_move(self, pm): 97 | MAU.update_macro(pm) 98 | 99 | def on_pmi_remove(self, pm): 100 | MAU.update_macro(pm) 101 | 102 | def on_pmi_paste(self, pm, pmi): 103 | MAU.update_macro(pm) 104 | 105 | def on_pmi_toggle(self, pm, pmi): 106 | MAU.update_macro(pm) 107 | 108 | def on_pmi_edit(self, pm, pmi): 109 | MAU.update_macro(pm) 110 | 111 | def get_pmi_icon(self, pm, pmi, idx): 112 | pr = prefs() 113 | icon = self.icon 114 | if pmi.icon: 115 | icon = pmi.icon 116 | elif pmi.text in pr.pie_menus: 117 | icon = pr.pie_menus[pmi.text].ed.icon 118 | 119 | return icon 120 | 121 | 122 | def register(): 123 | Editor() 124 | 125 | MAU.init_macros( 126 | PME_OT_macro_exec1, PME_OT_macro_exec_base, 127 | PME_OT_sticky_key_base, PME_OT_modal_base) 128 | -------------------------------------------------------------------------------- /ed_modal.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import pme 3 | from .constants import ( 4 | MODAL_CMD_MODES, W_PMI_HOTKEY, I_MODAL_PROP_MOVE, 5 | W_PMI_EXPR 6 | ) 7 | from .bl_utils import uname 8 | from .ed_base import EditorBase 9 | from .addon import temp_prefs 10 | from .modal_utils import encode_modal_data, decode_modal_data 11 | 12 | 13 | class PME_OT_prop_data_reset(bpy.types.Operator): 14 | bl_idname = "pme.prop_data_reset" 15 | bl_label = "Reset" 16 | bl_description = "Reset values" 17 | bl_options = {'INTERNAL'} 18 | 19 | def execute(self, context): 20 | tpr = temp_prefs() 21 | tpr.modal_item_prop_min = tpr.prop_data.min 22 | tpr.modal_item_prop_max = tpr.prop_data.max 23 | tpr.modal_item_prop_step = tpr.prop_data.step 24 | tpr.modal_item_prop_step_is_set = False 25 | return {'FINISHED'} 26 | 27 | 28 | pme.props.BoolProperty("mo", "confirm", False) 29 | pme.props.BoolProperty("mo", "block_ui", True) 30 | pme.props.BoolProperty("mo", "lock", True) 31 | 32 | 33 | class Editor(EditorBase): 34 | 35 | def __init__(self): 36 | self.id = 'MODAL' 37 | EditorBase.__init__(self) 38 | 39 | self.docs = "#Modal_Operator_Editor" 40 | self.use_slot_icon = False 41 | self.use_preview = False 42 | self.default_pmi_data = "mo?" 43 | self.supported_slot_modes = { 44 | 'COMMAND', 'PROP', 'INVOKE', 'FINISH', 'CANCEL', 'UPDATE'} 45 | self.supported_paste_modes = { 46 | 'MODAL' 47 | } 48 | 49 | def init_pm(self, pm): 50 | pass 51 | 52 | def on_pm_add(self, pm): 53 | pmi = pm.pmis.add() 54 | pmi.mode = 'INVOKE' 55 | pmi.name = bpy.types.UILayout.enum_item_name(pmi, "mode", 'INVOKE') 56 | 57 | def on_pmi_check(self, pm, pmi_data): 58 | EditorBase.on_pmi_check(self, pm, pmi_data) 59 | 60 | tpr = temp_prefs() 61 | if pmi_data.mode in {'COMMAND', 'PROP'}: 62 | if tpr.modal_item_custom: 63 | try: 64 | compile(tpr.modal_item_custom, "", "eval") 65 | except: 66 | pmi_data.info(W_PMI_EXPR) 67 | 68 | if pmi_data.mode == 'COMMAND' or \ 69 | pmi_data.mode == 'PROP' and tpr.modal_item_prop_mode == 'KEY': 70 | if tpr.modal_item_hk.key == 'NONE': 71 | pmi_data.info(W_PMI_HOTKEY) 72 | 73 | elif pmi_data.mode in MODAL_CMD_MODES: 74 | pmi_data.sname = bpy.types.UILayout.enum_item_name( 75 | pmi_data, "mode", pmi_data.mode) 76 | 77 | # elif pmi_data.mode == 'PROP': 78 | # pmi_data.sname = bpy.types.UILayout.enum_item_name( 79 | # tpr, "modal_item_prop_mode", tpr.modal_item_prop_mode) 80 | 81 | if pmi_data.mode == 'PROP' and tpr.modal_item_prop_mode == 'MOVE': 82 | pmi_data.info(I_MODAL_PROP_MOVE, False) 83 | 84 | def on_pmi_add(self, pm, pmi): 85 | pmi.mode = 'INVOKE' 86 | pmi.name = uname(pm.pmis, "Command", " ", 1, False) 87 | 88 | def on_pmi_pre_edit(self, pm, pmi, data): 89 | EditorBase.on_pmi_pre_edit(self, pm, pmi, data) 90 | 91 | tpr = temp_prefs() 92 | tpr.prop_data.clear() 93 | if pmi.mode == 'PROP': 94 | tpr.prop_data.init(pmi.text, pme.context.globals) 95 | 96 | tpr.modal_item_prop_min = tpr.prop_data.min 97 | tpr.modal_item_prop_max = tpr.prop_data.max 98 | tpr.modal_item_prop_step = tpr.prop_data.step 99 | tpr.modal_item_prop_step_is_set = False 100 | tpr.modal_item_custom = "" 101 | 102 | decode_modal_data(pmi, None, tpr) 103 | 104 | def on_pmi_edit(self, pm, pmi): 105 | if pmi.mode == 'COMMAND' and pmi.icon in {'', 'NONE'}: 106 | pmi.mode = 'INVOKE' 107 | 108 | if pmi.mode == 'PROP': 109 | encode_modal_data(pmi) 110 | elif pmi.mode == 'COMMAND': 111 | encode_modal_data(pmi) 112 | else: 113 | pmi.icon = "" 114 | 115 | def draw_extra_settings(self, layout, pm): 116 | EditorBase.draw_extra_settings(self, layout, pm) 117 | col = layout.column(align=True) 118 | col.prop(pm, "mo_confirm_on_release") 119 | col.prop(pm, "mo_block_ui") 120 | col.prop(pm, "mo_lock") 121 | 122 | def get_pmi_icon(self, pm, pmi, idx): 123 | icon = 'BLENDER' 124 | if pmi.mode == 'COMMAND': 125 | icon = 'FILE_SCRIPT' 126 | elif pmi.mode == 'PROP': 127 | if 'WHEELUPMOUSE' in pmi.icon or \ 128 | 'WHEELDOWNMOUSE' in pmi.icon: 129 | icon = 'DECORATE_OVERRIDE' 130 | elif pmi.icon.startswith('MOUSEMOVE'): 131 | icon = 'CENTER_ONLY' 132 | else: 133 | icon = 'ARROW_LEFTRIGHT' 134 | elif pmi.mode == 'INVOKE': 135 | icon = 'PLAY' 136 | elif pmi.mode == 'CANCEL': 137 | icon = 'CANCEL' 138 | elif pmi.mode == 'FINISH': 139 | icon = 'CHECKBOX_HLT' 140 | elif pmi.mode == 'UPDATE': 141 | icon = 'FILE_REFRESH' 142 | 143 | return icon 144 | 145 | 146 | def register(): 147 | Editor() 148 | -------------------------------------------------------------------------------- /ed_pie_menu.py: -------------------------------------------------------------------------------- 1 | from .ed_base import EditorBase 2 | from .constants import ARROW_ICONS 3 | from . import pme 4 | from .addon import ic, prefs 5 | from .layout_helper import lh 6 | 7 | 8 | pme.props.IntProperty("pm", "pm_radius", -1) 9 | pme.props.IntProperty("pm", "pm_confirm", -1) 10 | pme.props.IntProperty("pm", "pm_threshold", -1) 11 | pme.props.BoolProperty("pm", "pm_flick", True) 12 | 13 | 14 | class Editor(EditorBase): 15 | 16 | def __init__(self): 17 | self.id = 'PMENU' 18 | EditorBase.__init__(self) 19 | 20 | self.docs = "#Pie_Menu_Editor" 21 | self.default_pmi_data = "pm?" 22 | self.fixed_num_items = True 23 | self.use_swap = True 24 | self.supported_open_modes = {'PRESS', 'HOLD', 'DOUBLE_CLICK'} 25 | 26 | def on_pm_add(self, pm): 27 | for i in range(0, 10): 28 | pm.pmis.add() 29 | 30 | def on_pmi_rename(self, pm, pmi, old_name, name): 31 | pmi.name = name 32 | if not old_name and pmi.mode == 'EMPTY': 33 | pmi.mode = 'COMMAND' 34 | 35 | def draw_extra_settings(self, layout, pm): 36 | EditorBase.draw_extra_settings(self, layout, pm) 37 | col = layout.column(align=True) 38 | row = col.row(align=True) 39 | row.prop(pm, "pm_radius", text="Radius") 40 | if pm.pm_radius != -1: 41 | row.operator("pme.exec", text="", icon=ic('X')).cmd = \ 42 | "prefs().selected_pm.pm_radius = -1" 43 | if pm.pm_flick: 44 | row = col.row(align=True) 45 | row.prop(pm, "pm_threshold", text="Threshold") 46 | if pm.pm_threshold != -1: 47 | row.operator("pme.exec", text="", icon=ic('X')).cmd = \ 48 | "prefs().selected_pm.pm_threshold = -1" 49 | 50 | row = col.row(align=True) 51 | row.prop(pm, "pm_confirm", text="Confirm Threshold") 52 | if pm.pm_confirm != -1: 53 | row.operator("pme.exec", text="", icon=ic('X')).cmd = \ 54 | "prefs().selected_pm.pm_confirm = -1" 55 | layout.prop(pm, "pm_flick") 56 | 57 | # if pm.pm_radius != -1: 58 | # layout.label( 59 | # text="Custom radius disables pie menu animation", icon='INFO') 60 | 61 | def draw_items(self, layout, pm): 62 | pr = prefs() 63 | column = layout.column(align=True) 64 | 65 | for idx, pmi in enumerate(pm.pmis): 66 | lh.row(column, active=pmi.enabled) 67 | 68 | self.draw_item(pm, pmi, idx) 69 | self.draw_pmi_menu_btn(pr, idx) 70 | 71 | if idx == 7: 72 | column.separator() 73 | 74 | def get_pmi_icon(self, pm, pmi, idx): 75 | return ARROW_ICONS[idx] 76 | 77 | 78 | def register(): 79 | Editor() 80 | -------------------------------------------------------------------------------- /ed_stack_key.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .ed_base import EditorBase 3 | from . import pme 4 | 5 | pme.props.BoolProperty("s", "s_undo") 6 | pme.props.BoolProperty("s", "s_state") 7 | # pme.props.BoolProperty("s", "s_scroll", True) 8 | 9 | 10 | class Editor(EditorBase): 11 | 12 | def __init__(self): 13 | self.id = 'SCRIPT' 14 | EditorBase.__init__(self) 15 | 16 | self.docs = "#Stack_Key_Editor" 17 | self.use_slot_icon = False 18 | self.use_preview = False 19 | self.default_pmi_data = "s?" 20 | self.supported_slot_modes = {'COMMAND', 'HOTKEY'} 21 | 22 | def register_props(self, pm): 23 | self.register_pm_prop( 24 | "ed_undo", 25 | bpy.props.BoolProperty( 26 | name="Undo Previous Command", 27 | description="Undo previous command", 28 | get=lambda s: s.get_data("s_undo"), 29 | set=lambda s, v: s.set_data("s_undo", v) 30 | ) 31 | ) 32 | self.register_pm_prop( 33 | "ed_state", 34 | bpy.props.BoolProperty( 35 | name="Remember State", description="Remember state", 36 | get=lambda s: s.get_data("s_state"), 37 | set=lambda s, v: s.set_data("s_state", v) 38 | ) 39 | ) 40 | 41 | def init_pm(self, pm): 42 | if not pm.data.startswith("s?"): 43 | pm.data = self.default_pmi_data 44 | pmi = pm.pmis.add() 45 | pmi.text = pm.data 46 | pmi.mode = 'COMMAND' 47 | pmi.name = "Command 1" 48 | 49 | def on_pm_add(self, pm): 50 | pmi = pm.pmis.add() 51 | pmi.mode = 'COMMAND' 52 | pmi.name = "Command 1" 53 | 54 | def draw_extra_settings(self, layout, pm): 55 | EditorBase.draw_extra_settings(self, layout, pm) 56 | layout.prop(pm, "ed_undo") 57 | layout.prop(pm, "ed_state") 58 | 59 | 60 | def register(): 61 | Editor() 62 | -------------------------------------------------------------------------------- /ed_sticky_key.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import pme 3 | from .ed_base import EditorBase 4 | from .addon import prefs 5 | from .ui import tag_redraw 6 | from .operator_utils import find_statement 7 | 8 | 9 | class PME_OT_sticky_key_edit(bpy.types.Operator): 10 | bl_idname = "pme.sticky_key_edit" 11 | bl_label = "Save and Restore Previous Value" 12 | bl_description = "Save and restore the previous value" 13 | bl_options = {'INTERNAL'} 14 | 15 | pmi_prop = None 16 | pmi_value = None 17 | 18 | @staticmethod 19 | def parse_prop_value(text): 20 | prop, value = find_statement(text) 21 | if not prop: 22 | PME_OT_sticky_key_edit.pmi_prop = None 23 | PME_OT_sticky_key_edit.pmi_value = None 24 | else: 25 | PME_OT_sticky_key_edit.pmi_prop = prop 26 | PME_OT_sticky_key_edit.pmi_value = value 27 | 28 | def execute(self, context): 29 | cl = self.__class__ 30 | pr = prefs() 31 | pm = pr.selected_pm 32 | pm.pmis[1].mode = 'COMMAND' 33 | pm.pmis[1].text = cl.pmi_prop + " = value" 34 | pm.pmis[0].mode = 'COMMAND' 35 | pm.pmis[0].text = "value = %s; %s = %s" % ( 36 | cl.pmi_prop, cl.pmi_prop, cl.pmi_value) 37 | 38 | pr.pmi_data.info() 39 | pr.leave_mode() 40 | tag_redraw() 41 | 42 | return {'FINISHED'} 43 | 44 | 45 | pme.props.BoolProperty("sk", "sk_block_ui", False) 46 | 47 | 48 | class Editor(EditorBase): 49 | 50 | def __init__(self): 51 | self.id = 'STICKY' 52 | EditorBase.__init__(self) 53 | 54 | self.docs = "#Sticky_Key_Editor" 55 | self.use_slot_icon = False 56 | self.use_preview = False 57 | self.sub_item = False 58 | self.default_pmi_data = "sk?" 59 | self.fixed_num_items = True 60 | self.movable_items = False 61 | self.supported_slot_modes = {'COMMAND', 'HOTKEY'} 62 | self.toggleable_slots = False 63 | 64 | def init_pm(self, pm): 65 | pass 66 | 67 | def on_pm_add(self, pm): 68 | pmi = pm.pmis.add() 69 | pmi.mode = 'COMMAND' 70 | pmi.name = "On Press" 71 | pmi = pm.pmis.add() 72 | pmi.mode = 'COMMAND' 73 | pmi.name = "On Release" 74 | 75 | def on_pmi_check(self, pm, pmi_data): 76 | EditorBase.on_pmi_check(self, pm, pmi_data) 77 | 78 | if pmi_data.mode == 'COMMAND': 79 | PME_OT_sticky_key_edit.parse_prop_value(pmi_data.cmd) 80 | 81 | def draw_extra_settings(self, layout, pm): 82 | EditorBase.draw_extra_settings(self, layout, pm) 83 | layout.prop(pm, "sk_block_ui") 84 | 85 | def get_pmi_icon(self, pm, pmi, idx): 86 | return 'TRIA_DOWN_BAR' if idx == 0 else 'TRIA_UP' 87 | 88 | 89 | def register(): 90 | Editor() 91 | -------------------------------------------------------------------------------- /examples/3d_view_numpad_pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.17", 3 | "menus": [ 4 | [ 5 | "3D View Numpad", 6 | "3D View", 7 | "Q", 8 | [ 9 | [ 10 | "Left", 11 | "COMMAND", 12 | "TRIA_LEFT", 13 | "bpy.ops.view3d.view_axis(type='LEFT')" 14 | ], 15 | [ 16 | "Right", 17 | "COMMAND", 18 | "TRIA_RIGHT", 19 | "bpy.ops.view3d.view_axis(type='RIGHT')" 20 | ], 21 | [ 22 | "Bottom", 23 | "COMMAND", 24 | "TRIA_DOWN", 25 | "bpy.ops.view3d.view_axis(type='BOTTOM')" 26 | ], 27 | [ 28 | "Top", 29 | "COMMAND", 30 | "TRIA_UP", 31 | "bpy.ops.view3d.view_axis(type='TOP')" 32 | ], 33 | [ 34 | "Front", 35 | "COMMAND", 36 | "RADIOBUT_ON", 37 | "bpy.ops.view3d.view_axis(type='FRONT')" 38 | ], 39 | [ 40 | "Back", 41 | "COMMAND", 42 | "RADIOBUT_OFF", 43 | "bpy.ops.view3d.view_axis(type='BACK')" 44 | ], 45 | [ 46 | "Camera", 47 | "COMMAND", 48 | "CAMERA_DATA", 49 | "bpy.ops.view3d.view_camera(type='CAMERA')" 50 | ], 51 | [ 52 | "Persp/Ortho", 53 | "COMMAND", 54 | "MESH_GRID", 55 | "bpy.ops.view3d.view_persportho()" 56 | ] 57 | ], 58 | "PMENU", 59 | "pm?", 60 | "PRESS", 61 | "", 62 | "Examples" 63 | ] 64 | ] 65 | } -------------------------------------------------------------------------------- /examples/3d_view_trackball_rotate_macro.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.18", 3 | "menus": [ 4 | [ 5 | "Trackball Rotate View Macro", 6 | "3D View", 7 | "ctrl+shift+MIDDLEMOUSE", 8 | [ 9 | [ 10 | "Orbit Method = 'TRACKBALL'", 11 | "COMMAND", 12 | "TEXT", 13 | "vrm = C.preferences.inputs.view_rotate_method; C.preferences.inputs.view_rotate_method = 'TRACKBALL'" 14 | ], 15 | [ 16 | "Rotate View", 17 | "COMMAND", 18 | "BLENDER", 19 | "bpy.ops.view3d.rotate()" 20 | ], 21 | [ 22 | "Restore Orbit Method", 23 | "COMMAND", 24 | "TEXT", 25 | "C.preferences.inputs.view_rotate_method = vrm" 26 | ] 27 | ], 28 | "MACRO", 29 | "m?", 30 | "PRESS", 31 | "", 32 | "" 33 | ] 34 | ] 35 | } -------------------------------------------------------------------------------- /examples/context_sensitive_menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.16", 3 | "menus": [ 4 | [ 5 | "CSM: Mesh", 6 | "Window", 7 | "", 8 | [ 9 | [ 10 | "" 11 | ], 12 | [ 13 | "" 14 | ], 15 | [ 16 | "" 17 | ], 18 | [ 19 | "" 20 | ], 21 | [ 22 | "" 23 | ], 24 | [ 25 | "" 26 | ], 27 | [ 28 | "" 29 | ], 30 | [ 31 | "" 32 | ] 33 | ], 34 | "PMENU", 35 | "pm?", 36 | "PRESS", 37 | "", 38 | "Examples" 39 | ], 40 | [ 41 | "Context Sensitive Menu", 42 | "Object Non-modal", 43 | "F1", 44 | [ 45 | [ 46 | "Open Menu by Selected Object's Type", 47 | "COMMAND", 48 | "", 49 | "execute_script(\"scripts/command_context_sensitive_menu.py\", prefix=\"CSM: \")" 50 | ] 51 | ], 52 | "SCRIPT", 53 | "s?", 54 | "PRESS", 55 | "", 56 | "Examples" 57 | ], 58 | [ 59 | "CSM: None Object", 60 | "Window", 61 | "", 62 | [ 63 | [ 64 | "" 65 | ], 66 | [ 67 | "" 68 | ], 69 | [ 70 | "" 71 | ], 72 | [ 73 | "" 74 | ], 75 | [ 76 | "" 77 | ], 78 | [ 79 | "" 80 | ], 81 | [ 82 | "" 83 | ], 84 | [ 85 | "" 86 | ] 87 | ], 88 | "PMENU", 89 | "pm?", 90 | "PRESS", 91 | "", 92 | "Examples" 93 | ], 94 | [ 95 | "CSM: Any Object", 96 | "Window", 97 | "", 98 | [ 99 | [ 100 | "" 101 | ], 102 | [ 103 | "" 104 | ], 105 | [ 106 | "" 107 | ], 108 | [ 109 | "" 110 | ], 111 | [ 112 | "" 113 | ], 114 | [ 115 | "" 116 | ], 117 | [ 118 | "" 119 | ], 120 | [ 121 | "" 122 | ] 123 | ], 124 | "PMENU", 125 | "pm?", 126 | "PRESS", 127 | "", 128 | "Examples" 129 | ] 130 | ] 131 | } -------------------------------------------------------------------------------- /examples/mesh_connect_2_vertices_macro.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.16", 3 | "menus": [ 4 | [ 5 | "Double Select", 6 | "Mesh", 7 | "", 8 | [ 9 | [ 10 | "Activate/Select", 11 | "COMMAND", 12 | "", 13 | "bpy.ops.view3d.select()" 14 | ], 15 | [ 16 | "Activate/Select", 17 | "COMMAND", 18 | "", 19 | "bpy.ops.view3d.select(extend=True)" 20 | ] 21 | ], 22 | "STICKY", 23 | "sk?sk_block_ui=True", 24 | "PRESS", 25 | "", 26 | "Examples" 27 | ], 28 | [ 29 | "Connect 2 Vertices", 30 | "Mesh", 31 | "alt+oskey+LEFTMOUSE", 32 | [ 33 | [ 34 | "Switch to Vertex Select Mode", 35 | "COMMAND", 36 | "TEXT", 37 | "msm = C.tool_settings.mesh_select_mode[:]; C.tool_settings.mesh_select_mode = (True, False, False)" 38 | ], 39 | [ 40 | "Double Select", 41 | "MENU", 42 | "", 43 | "Double Select" 44 | ], 45 | [ 46 | "Vertex Connect", 47 | "COMMAND", 48 | "BLENDER", 49 | "bpy.ops.mesh.vert_connect()" 50 | ], 51 | [ 52 | "Restore Select Mode", 53 | "COMMAND", 54 | "TEXT", 55 | "C.tool_settings.mesh_select_mode = msm" 56 | ] 57 | ], 58 | "MACRO", 59 | "m?", 60 | "PRESS", 61 | "", 62 | "Examples" 63 | ] 64 | ] 65 | } -------------------------------------------------------------------------------- /examples/mesh_edge_crease_and_bevel_weight_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.16", 3 | "menus": [ 4 | [ 5 | "Edge Crease", 6 | "Window", 7 | "", 8 | [ 9 | [ 10 | "GET", 11 | "COMMAND", 12 | "", 13 | "import bmesh; obj = C.edit_object; me = obj and obj.type == 'MESH' and obj.data; bm = me and bmesh.from_edit_mesh(me); l = me and bm.edges.layers.crease.verify(); e = me and find_by(bm.edges, \"select\", True); return e[l] if e else 0" 14 | ], 15 | [ 16 | "min", 17 | "", 18 | "0.0" 19 | ], 20 | [ 21 | "max", 22 | "", 23 | "1.0" 24 | ], 25 | [ 26 | "SET", 27 | "COMMAND", 28 | "", 29 | "import bmesh; obj = C.edit_object; me = obj and obj.type == 'MESH' and obj.data; bm = me and bmesh.from_edit_mesh(me); l = me and bm.edges.layers.crease.verify(); me and [e.__setitem__(l, value) for e in bm.edges if e.select]; me and bmesh.update_edit_mesh(me)" 30 | ], 31 | [ 32 | "step", 33 | "", 34 | "5.0" 35 | ] 36 | ], 37 | "PROPERTY", 38 | "prop?", 39 | "PRESS", 40 | "FLOAT", 41 | "Examples" 42 | ], 43 | [ 44 | "Edge Bevel Weight", 45 | "Window", 46 | "", 47 | [ 48 | [ 49 | "step", 50 | "", 51 | "5.0" 52 | ], 53 | [ 54 | "min", 55 | "", 56 | "0.0" 57 | ], 58 | [ 59 | "max", 60 | "", 61 | "1.0" 62 | ], 63 | [ 64 | "GET", 65 | "COMMAND", 66 | "", 67 | "import bmesh; obj = C.edit_object; me = obj and obj.type == 'MESH' and obj.data; bm = me and bmesh.from_edit_mesh(me); l = me and bm.edges.layers.bevel_weight.verify(); e = me and find_by(bm.edges, \"select\", True); return e[l] if e else 0" 68 | ], 69 | [ 70 | "SET", 71 | "COMMAND", 72 | "", 73 | "import bmesh; obj = C.edit_object; me = obj and obj.type == 'MESH' and obj.data; bm = me and bmesh.from_edit_mesh(me); l = me and bm.edges.layers.bevel_weight.verify(); me and [e.__setitem__(l, value) for e in bm.edges if e.select]; me and bmesh.update_edit_mesh(me)" 74 | ] 75 | ], 76 | "PROPERTY", 77 | "prop?", 78 | "PRESS", 79 | "FLOAT", 80 | "Examples" 81 | ] 82 | ] 83 | } -------------------------------------------------------------------------------- /examples/mesh_grid_fill_modal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.16", 3 | "menus": [ 4 | [ 5 | "Grid Fill Modal", 6 | "Window", 7 | "", 8 | [ 9 | [ 10 | "Grid Fill", 11 | "INVOKE", 12 | "", 13 | "bpy.ops.mesh.fill_grid()" 14 | ], 15 | [ 16 | "On Update", 17 | "UPDATE", 18 | "", 19 | "bpy.ops.ed.undo_redo()" 20 | ], 21 | [ 22 | "Span", 23 | "PROP", 24 | "ctrl+WHEELUPMOUSE;;;;", 25 | "C.active_operator.properties.span" 26 | ], 27 | [ 28 | "Offset", 29 | "PROP", 30 | "WHEELUPMOUSE;;;;", 31 | "C.active_operator.properties.offset" 32 | ], 33 | [ 34 | "Cancel", 35 | "COMMAND", 36 | "RIGHTMOUSE;", 37 | "bpy.ops.ed.undo(); cancel()" 38 | ], 39 | [ 40 | "Cancel", 41 | "COMMAND", 42 | "ESC;", 43 | "bpy.ops.ed.undo(); cancel()" 44 | ] 45 | ], 46 | "MODAL", 47 | "mo?", 48 | "PRESS", 49 | "", 50 | "Examples" 51 | ] 52 | ] 53 | } -------------------------------------------------------------------------------- /examples/mesh_lasso_dissolve_macro.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.17", 3 | "menus": [ 4 | [ 5 | "Lasso Dissolve", 6 | "Mesh", 7 | "ctrl+oskey+LEFTMOUSE", 8 | [ 9 | [ 10 | "Switch to Face Select Mode", 11 | "COMMAND", 12 | "TEXT", 13 | "msm = C.tool_settings.mesh_select_mode[:]; C.tool_settings.mesh_select_mode = (False, False, True)" 14 | ], 15 | [ 16 | "Lasso Select", 17 | "COMMAND", 18 | "BLENDER", 19 | "bpy.ops.view3d.select_lasso()" 20 | ], 21 | [ 22 | "Dissolve Faces", 23 | "COMMAND", 24 | "BLENDER", 25 | "bpy.ops.mesh.dissolve_faces(use_verts=True)" 26 | ], 27 | [ 28 | "Restore Select Mode", 29 | "COMMAND", 30 | "TEXT", 31 | "C.tool_settings.mesh_select_mode = msm" 32 | ] 33 | ], 34 | "MACRO", 35 | "m?", 36 | "PRESS", 37 | "", 38 | "Examples" 39 | ] 40 | ] 41 | } -------------------------------------------------------------------------------- /examples/mesh_select_mode_pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.17", 3 | "menus": [ 4 | [ 5 | "Mesh Select Mode", 6 | "Mesh", 7 | "ONE", 8 | [ 9 | [ 10 | "Edge", 11 | "COMMAND", 12 | "EDGESEL", 13 | "bpy.context.tool_settings.mesh_select_mode = (False,True,False); tag_redraw(area='VIEW_3D', region='WINDOW')" 14 | ], 15 | [ 16 | "Face", 17 | "COMMAND", 18 | "FACESEL", 19 | "bpy.context.tool_settings.mesh_select_mode = (False,False,True); tag_redraw(area='VIEW_3D', region='WINDOW')" 20 | ], 21 | [ 22 | "Vertex", 23 | "COMMAND", 24 | "VERTEXSEL", 25 | "bpy.context.tool_settings.mesh_select_mode = (True,False,False); tag_redraw(area='VIEW_3D', region='WINDOW')" 26 | ], 27 | [ 28 | "All", 29 | "COMMAND", 30 | "OBJECT_DATA", 31 | "bpy.context.tool_settings.mesh_select_mode = (True,True,True); tag_redraw(area='VIEW_3D', region='WINDOW')" 32 | ], 33 | [ 34 | "" 35 | ], 36 | [ 37 | "Edge, Face", 38 | "COMMAND", 39 | "", 40 | "bpy.context.tool_settings.mesh_select_mode = (False,True,True); tag_redraw(area='VIEW_3D', region='WINDOW')" 41 | ], 42 | [ 43 | "Vertex, Edge", 44 | "COMMAND", 45 | "", 46 | "bpy.context.tool_settings.mesh_select_mode = (True,True,False); tag_redraw(area='VIEW_3D', region='WINDOW')" 47 | ], 48 | [ 49 | "Vertex, Face", 50 | "COMMAND", 51 | "", 52 | "bpy.context.tool_settings.mesh_select_mode = (True,False,True); tag_redraw(area='VIEW_3D', region='WINDOW')" 53 | ] 54 | ], 55 | "PMENU", 56 | "pm?", 57 | "TWEAK", 58 | "", 59 | "Examples" 60 | ] 61 | ] 62 | } -------------------------------------------------------------------------------- /examples/object_mode_pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.16", 3 | "menus": [ 4 | [ 5 | "Object Mode", 6 | "Object Non-modal", 7 | "TAB", 8 | [ 9 | [ 10 | "Object Mode", 11 | "COMMAND", 12 | "OBJECT_DATA", 13 | "bpy.ops.object.mode_set(mode='OBJECT')" 14 | ], 15 | [ 16 | "Edit Mode", 17 | "COMMAND", 18 | "EDITMODE_HLT", 19 | "bpy.ops.object.mode_set(mode='EDIT')" 20 | ], 21 | [ 22 | "Sculpt Mode", 23 | "COMMAND", 24 | "SCULPTMODE_HLT", 25 | "bpy.ops.object.mode_set(mode='SCULPT')" 26 | ], 27 | [ 28 | "Vertex Paint", 29 | "COMMAND", 30 | "VPAINT_HLT", 31 | "bpy.ops.object.mode_set(mode='VERTEX_PAINT')" 32 | ], 33 | [ 34 | "Weight Paint", 35 | "COMMAND", 36 | "WPAINT_HLT", 37 | "bpy.ops.object.mode_set(mode='WEIGHT_PAINT')" 38 | ], 39 | [ 40 | "Texture Paint", 41 | "COMMAND", 42 | "TPAINT_HLT", 43 | "bpy.ops.object.mode_set(mode='TEXTURE_PAINT')" 44 | ], 45 | [ 46 | "Pose Mode", 47 | "COMMAND", 48 | "POSE_HLT", 49 | "bpy.ops.object.mode_set(mode='POSE')" 50 | ], 51 | [ 52 | "Particle Edit", 53 | "COMMAND", 54 | "PARTICLEMODE", 55 | "bpy.ops.object.mode_set(mode='PARTICLE_EDIT')" 56 | ] 57 | ], 58 | "PMENU", 59 | "pm?", 60 | "TWEAK", 61 | "", 62 | "Examples" 63 | ] 64 | ] 65 | } -------------------------------------------------------------------------------- /examples/toolbar_top.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.18.6", 3 | "menus": [ 4 | [ 5 | "TOPBAR_HT_upper_bar_right", 6 | "Window", 7 | "", 8 | [ 9 | [ 10 | "row?" 11 | ], 12 | [ 13 | "Object", 14 | "CUSTOM", 15 | "", 16 | "ao = C.active_object; L.enabled = ao and ao.mode != 'EDIT'; ao and L.template_ID(C.view_layer.objects, \"active\")", 17 | 0 18 | ], 19 | [ 20 | "spacer?hsep=SPACER" 21 | ], 22 | [ 23 | "Data", 24 | "CUSTOM", 25 | "", 26 | "ao = C.active_object; L.enabled = ao and ao.mode != 'EDIT'; ao and L.template_ID(ao, \"data\")", 27 | 0 28 | ], 29 | [ 30 | "spacer?hsep=SPACER" 31 | ], 32 | [ 33 | "Transform", 34 | "COMMAND", 35 | "#OBJECT_DATAMODE", 36 | "bpy.ops.pme.popup_panel(panel='OBJECT_PT_transform,OBJECT_PT_display,OBJECT_PT_relations', frame=True, area='PROPERTIES')", 37 | 0 38 | ], 39 | [ 40 | "3D Cursor", 41 | "COMMAND", 42 | "#GRID", 43 | "bpy.ops.pme.popup_panel(panel='VIEW3D_PT_view3d_cursor,VIEW3D_PT_view3d_properties', frame=True, area='VIEW_3D')", 44 | 0 45 | ], 46 | [ 47 | "spacer?hsep=SPACER" 48 | ], 49 | [ 50 | "spacer?hsep=ALIGNER" 51 | ], 52 | [ 53 | "Interactive Panels", 54 | "PROP", 55 | "#WINDOW", 56 | "prefs().interactive_panels", 57 | 0 58 | ], 59 | [ 60 | "PME", 61 | "COMMAND", 62 | "", 63 | "bpy.ops.pme.popup_addon_preferences(addon='pie_menu_editor', center=False)", 64 | 0 65 | ], 66 | [ 67 | "Show Preferences", 68 | "COMMAND", 69 | "#PREFERENCES", 70 | "bpy.ops.screen.userpref_show()", 71 | 0 72 | ] 73 | ], 74 | "DIALOG", 75 | "pd?pd_panel=2", 76 | "PRESS", 77 | "", 78 | "Examples" 79 | ] 80 | ] 81 | } -------------------------------------------------------------------------------- /examples/window_area_pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.18.6", 3 | "menus": [ 4 | [ 5 | "Window Area", 6 | "Window", 7 | "ctrl+SPACE, NONE", 8 | [ 9 | [ 10 | "Tools", 11 | "HOTKEY", 12 | "PREFERENCES", 13 | "T", 14 | 0 15 | ], 16 | [ 17 | "Properties", 18 | "HOTKEY", 19 | "SETTINGS", 20 | "N", 21 | 0 22 | ], 23 | [ 24 | "Header", 25 | "COMMAND", 26 | "TRIA_DOWN", 27 | "r = C.area.regions[0]; sd = C.space_data; on_top = r.y > C.area.y; O.pme.exec_override(cmd='O.screen.region_flip()', kwargs='d=dict(region=C.area.regions[0])') if on_top else None; sd.show_region_header = on_top or not sd.show_region_header", 28 | 0 29 | ], 30 | [ 31 | "Header", 32 | "COMMAND", 33 | "TRIA_UP", 34 | "r = C.area.regions[0]; sd = C.space_data; on_top = r.y > C.area.y; O.pme.exec_override(cmd='O.screen.region_flip()', kwargs='d=dict(region=C.area.regions[0])') if not on_top else None; sd.show_region_header = not on_top or not sd.show_region_header", 35 | 0 36 | ], 37 | [ 38 | "Maximize", 39 | "COMMAND", 40 | "FULLSCREEN_ENTER", 41 | "bpy.ops.screen.screen_full_area()", 42 | 0 43 | ], 44 | [ 45 | "Fullscreen", 46 | "COMMAND", 47 | "MESH_PLANE", 48 | "bpy.ops.screen.screen_full_area(use_hide_panels=True)", 49 | 0 50 | ], 51 | [ 52 | "Menus", 53 | "COMMAND", 54 | "COLLAPSEMENU", 55 | "bpy.ops.screen.header_toggle_menus()", 56 | 0 57 | ], 58 | [ 59 | "Render View", 60 | "COMMAND", 61 | "RENDER_RESULT", 62 | "bpy.ops.render.view_show()", 63 | 0 64 | ], 65 | [ 66 | "" 67 | ], 68 | [ 69 | "" 70 | ] 71 | ], 72 | "PMENU", 73 | "pm?", 74 | "PRESS", 75 | "", 76 | "Examples" 77 | ] 78 | ] 79 | } -------------------------------------------------------------------------------- /icons/p1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/p1.png -------------------------------------------------------------------------------- /icons/p2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/p2.png -------------------------------------------------------------------------------- /icons/p3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/p3.png -------------------------------------------------------------------------------- /icons/p4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/p4.png -------------------------------------------------------------------------------- /icons/p6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/p6.png -------------------------------------------------------------------------------- /icons/p7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/p7.png -------------------------------------------------------------------------------- /icons/p8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/p8.png -------------------------------------------------------------------------------- /icons/p9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/p9.png -------------------------------------------------------------------------------- /icons/pA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/pA.png -------------------------------------------------------------------------------- /icons/pB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/pB.png -------------------------------------------------------------------------------- /icons/pChord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/pChord.png -------------------------------------------------------------------------------- /icons/pDouble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/pDouble.png -------------------------------------------------------------------------------- /icons/pHold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/pHold.png -------------------------------------------------------------------------------- /icons/pPress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/pPress.png -------------------------------------------------------------------------------- /icons/pTweak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pluglug/pie-menu-editor-fork/43990ce5de1a2764ef2cdcb867d5e6817bddf1c2/icons/pTweak.png -------------------------------------------------------------------------------- /macro_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import re 3 | from . import operator_utils 4 | from .debug_utils import * 5 | from .addon import prefs, print_exc 6 | from .bl_utils import uname 7 | 8 | 9 | _operators = {} 10 | _macros = {} 11 | _macro_execs = [] 12 | _exec_base = None 13 | _sticky_op = None 14 | _modal_op = None 15 | 16 | 17 | def init_macros(exec1, base, sticky, modal): 18 | _macro_execs.append(exec1) 19 | global _exec_base, _sticky_op, _modal_op 20 | _exec_base = base 21 | _sticky_op = sticky 22 | _modal_op = modal 23 | 24 | 25 | def add_macro_exec(): 26 | id = "macro_exec%d" % (len(_macro_execs) + 1) 27 | tp_name = "PME_OT_" + id 28 | defs = { 29 | "bl_idname": "pme." + id, 30 | } 31 | 32 | tp = type(tp_name, (_exec_base, bpy.types.Operator), defs) 33 | 34 | bpy.utils.register_class(tp) 35 | _macro_execs.append(tp) 36 | 37 | 38 | def _gen_tp_id(name): 39 | def repl(mo): 40 | c = mo.group(0) 41 | try: 42 | cc = ord(c) 43 | except: 44 | return "_" 45 | 46 | return chr(97 + cc % 26) 47 | 48 | name = name.replace(" ", "_") 49 | name = name.lower() 50 | pre_tp, pre_id = "PME_OT_", "pme." 51 | id = "macro_" + re.sub(r"[^_a-z0-9]", repl, name, flags=re.I) 52 | id = uname(bpy.types, pre_tp + id, sep="_")[len(pre_tp):] 53 | return pre_tp + id, pre_id + id 54 | 55 | 56 | def _gen_op(tp, idx, **kwargs): 57 | tpname = tp.__name__[:-5] + str(idx + 1) 58 | bl_idname = tp.bl_idname + str(idx + 1) 59 | 60 | if tpname not in _operators: 61 | defs = dict(bl_idname=bl_idname) 62 | defs.update(kwargs) 63 | new_tp = type(tpname, (tp, bpy.types.Operator), defs) 64 | bpy.utils.register_class(new_tp) 65 | _operators[tpname] = new_tp 66 | 67 | return tpname 68 | 69 | 70 | def _gen_modal_op(pm, idx): 71 | lock = pm.get_data("lock") 72 | 73 | tpname = _modal_op.__name__[:-5] 74 | bl_idname = _modal_op.bl_idname 75 | if lock: 76 | tpname += "_grab" 77 | bl_idname += "_grab" 78 | 79 | tpname += str(idx + 1) 80 | bl_idname += str(idx + 1) 81 | 82 | if tpname not in _operators: 83 | defs = dict(bl_idname=bl_idname) 84 | if not lock: 85 | defs["bl_options"] = {'REGISTER'} 86 | new_tp = type(tpname, (_modal_op, bpy.types.Operator), defs) 87 | bpy.utils.register_class(new_tp) 88 | _operators[tpname] = new_tp 89 | 90 | return tpname 91 | 92 | 93 | def add_macro(pm): 94 | if pm.name in _macros: 95 | return 96 | 97 | pr = prefs() 98 | tp_name, tp_bl_idname = _gen_tp_id(pm.name) 99 | 100 | DBG_MACRO and logh("Add Macro: %s (%s)" % (pm.name, tp_name)) 101 | 102 | defs = { 103 | "bl_label": pm.name, 104 | "bl_idname": tp_bl_idname, 105 | # "bl_options": {'REGISTER', 'UNDO', 'MACRO'}, 106 | "bl_options": {'REGISTER', 'UNDO'}, 107 | } 108 | 109 | tp = type(tp_name, (bpy.types.Macro,), defs) 110 | 111 | try: 112 | bpy.utils.register_class(tp) 113 | _macros[pm.name] = tp 114 | 115 | idx, sticky_idx, modal_idx = 1, 0, 0 116 | for pmi in pm.pmis: 117 | if not pmi.enabled: 118 | continue 119 | 120 | pmi.icon = '' 121 | if pmi.mode == 'COMMAND': 122 | sub_op_idname, _, pos_args = operator_utils.find_operator( 123 | pmi.text) 124 | 125 | sub_op_exec_ctx, _ = operator_utils.parse_pos_args(pos_args) 126 | 127 | if sub_op_idname and sub_op_exec_ctx.startswith('INVOKE'): 128 | sub_tp = eval("bpy.ops." + sub_op_idname).idname() 129 | pmi.icon = 'BLENDER' 130 | DBG_MACRO and logi("Type", sub_tp) 131 | tp.define(sub_tp) 132 | else: 133 | while len(_macro_execs) < idx: 134 | add_macro_exec() 135 | pmi.icon = 'TEXT' 136 | DBG_MACRO and logi("Command", pmi.text) 137 | tp.define("PME_OT_macro_exec%d" % idx) 138 | idx += 1 139 | 140 | elif pmi.mode == 'MENU': 141 | if pmi.text not in pr.pie_menus: 142 | continue 143 | sub_pm = pr.pie_menus[pmi.text] 144 | if sub_pm.mode == 'MACRO': 145 | sub_tp = _macros.get(sub_pm.name, None) 146 | if sub_tp: 147 | DBG_MACRO and logi("Macro", sub_pm.name) 148 | tp.define(sub_tp.__name__) 149 | 150 | elif sub_pm.mode == 'MODAL': 151 | DBG_MACRO and logi("Modal", sub_pm.name) 152 | idname = _gen_modal_op(sub_pm, modal_idx) 153 | tp.define(idname) 154 | modal_idx += 1 155 | 156 | elif sub_pm.mode == 'STICKY': 157 | DBG_MACRO and logi("Sticky", sub_pm.name) 158 | idname = _gen_op(_sticky_op, sticky_idx) 159 | tp.define(idname) 160 | sticky_idx += 1 161 | 162 | except: 163 | print_exc() 164 | 165 | 166 | def remove_macro(pm): 167 | if pm.name not in _macros: 168 | return 169 | 170 | bpy.utils.unregister_class(_macros[pm.name]) 171 | del _macros[pm.name] 172 | 173 | 174 | def remove_all_macros(): 175 | for v in _macros.values(): 176 | bpy.utils.unregister_class(v) 177 | _macros.clear() 178 | 179 | while len(_macro_execs) > 1: 180 | bpy.utils.unregister_class(_macro_execs.pop()) 181 | _macro_execs.clear() 182 | 183 | 184 | def update_macro(pm): 185 | if pm.name not in _macros: 186 | return 187 | 188 | remove_macro(pm) 189 | add_macro(pm) 190 | 191 | 192 | def _fill_props(props, pm, idx=1): 193 | pr = prefs() 194 | 195 | sticky_idx, modal_idx = 0, 0 196 | for pmi in pm.pmis: 197 | if not pmi.enabled: 198 | continue 199 | 200 | if pmi.mode == 'COMMAND': 201 | sub_op_idname, args, pos_args = operator_utils.find_operator( 202 | pmi.text) 203 | 204 | sub_op_exec_ctx, _ = operator_utils.parse_pos_args(pos_args) 205 | 206 | if sub_op_idname and sub_op_exec_ctx.startswith('INVOKE'): 207 | args = ",".join(args) 208 | sub_tp = eval("bpy.ops." + sub_op_idname).idname() 209 | 210 | props[sub_tp] = eval("dict(%s)" % args) 211 | else: 212 | # while len(_macro_execs) < idx: 213 | # add_macro_exec() 214 | props["PME_OT_macro_exec%d" % idx] = dict(cmd=pmi.text) 215 | idx += 1 216 | 217 | elif pmi.mode == 'MENU': 218 | sub_pm = pr.pie_menus[pmi.text] 219 | if sub_pm.mode == 'STICKY': 220 | props[_gen_op(_sticky_op, sticky_idx)] = \ 221 | dict(pm_name=sub_pm.name) 222 | sticky_idx += 1 223 | 224 | elif sub_pm.mode == 'MODAL': 225 | idname = _gen_modal_op(sub_pm, modal_idx) 226 | props[idname] = dict(pm_name=sub_pm.name) 227 | modal_idx += 1 228 | 229 | elif sub_pm.mode == 'MACRO': 230 | sub_props = {} 231 | _fill_props(sub_props, sub_pm) 232 | props[_macros[sub_pm.name].__name__] = sub_props 233 | 234 | 235 | def execute_macro(pm): 236 | if pm.name not in _macros: 237 | return 238 | 239 | tp = _macros[pm.name] 240 | op = eval("bpy.ops." + tp.bl_idname) 241 | props = {} 242 | _fill_props(props, pm) 243 | op('INVOKE_DEFAULT', True, **props) 244 | 245 | 246 | def rename_macro(old_name, name): 247 | if old_name not in _macros: 248 | return 249 | 250 | _macros[name] = _macros[old_name] 251 | _macros[name].bl_label = name 252 | del _macros[old_name] 253 | 254 | bpy.utils.unregister_class(_macros[name]) 255 | 256 | tp_name, tp_bl_idname = _gen_tp_id(name) 257 | _macros[name].__name__ = tp_name 258 | _macros[name].bl_idname = tp_bl_idname 259 | bpy.utils.register_class(_macros[name]) 260 | 261 | 262 | def register(): 263 | pass 264 | 265 | 266 | def unregister(): 267 | remove_all_macros() 268 | 269 | for v in _operators.values(): 270 | bpy.utils.unregister_class(v) 271 | _operators.clear() 272 | -------------------------------------------------------------------------------- /modal_utils.py: -------------------------------------------------------------------------------- 1 | from .addon import temp_prefs 2 | from .utils import isclose 3 | 4 | 5 | def encode_modal_data(pmi): 6 | cmd = pmi.mode == 'COMMAND' 7 | 8 | tpr = temp_prefs() 9 | # data = tpr.modal_item_prop_mode 10 | data = [tpr.modal_item_hk.to_string()] 11 | if cmd: 12 | data.append(tpr.modal_item_custom) 13 | 14 | elif tpr.modal_item_hk.key != 'NONE': 15 | data.append( 16 | "" if isclose(tpr.modal_item_prop_min, tpr.prop_data.min) else 17 | str(tpr.modal_item_prop_min)) 18 | data.append( 19 | "" if isclose(tpr.modal_item_prop_max, tpr.prop_data.max) else 20 | str(tpr.modal_item_prop_max)) 21 | data.append( 22 | "" if not tpr.modal_item_prop_step_is_set else 23 | str(tpr.modal_item_prop_step)) 24 | data.append(tpr.modal_item_custom) 25 | 26 | pmi.icon = ";".join(data) 27 | 28 | 29 | def decode_modal_data(pmi, prop_data=None, tpr=None): 30 | hk, min_value, max_value, step = None, None, None, None 31 | data = pmi.icon 32 | cmd = pmi.mode == 'COMMAND' 33 | custom = "" 34 | 35 | if data: 36 | if cmd: 37 | data = data.split(";", 4) 38 | hk = data[0] 39 | 40 | tpr and tpr.modal_item_hk.from_string(hk) 41 | n = len(data) 42 | if n > 1: 43 | custom = data[1] 44 | tpr and setattr(tpr, "modal_item_custom", custom) 45 | prop_data and setattr(prop_data, "custom", custom) 46 | 47 | return hk, 0, 0, 1, custom 48 | 49 | else: 50 | prop_data and setattr(prop_data, "icon", data) 51 | data = data.split(";", 4) 52 | hk = data[0] 53 | 54 | tpr and tpr.modal_item_hk.from_string(hk) 55 | n = len(data) 56 | if n > 1 and data[1]: 57 | min_value = float(data[1]) 58 | tpr and setattr(tpr, "modal_item_prop_min", min_value) 59 | prop_data and setattr(prop_data, "min", min_value) 60 | if n > 2 and data[2]: 61 | max_value = float(data[2]) 62 | tpr and setattr(tpr, "modal_item_prop_max", max_value) 63 | prop_data and setattr(prop_data, "max", max_value) 64 | if n > 3 and data[3]: 65 | step = float(data[3]) 66 | tpr and setattr(tpr, "modal_item_prop_step", step) 67 | prop_data and setattr(prop_data, "_step", step) 68 | if n > 4: 69 | custom = data[4] 70 | if tpr: 71 | tpr["modal_item_custom"] = custom 72 | prop_data and setattr(prop_data, "custom", custom) 73 | else: 74 | tpr and tpr.modal_item_hk.clear() 75 | 76 | if tpr and not cmd: 77 | if tpr.modal_item_hk.key in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}: 78 | tpr["modal_item_prop_mode"] = 2 79 | elif tpr.modal_item_hk.key == 'MOUSEMOVE': 80 | tpr["modal_item_prop_mode"] = 1 81 | else: 82 | tpr["modal_item_prop_mode"] = 0 83 | 84 | return hk, min_value, max_value, step, custom 85 | -------------------------------------------------------------------------------- /operator_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from io import BytesIO 3 | from itertools import chain 4 | from tokenize import ( 5 | tokenize, Untokenizer, 6 | OP, ENCODING, NAME, STRING, NUMBER, ENDMARKER, INDENT, DEDENT, NEWLINE, NL 7 | ) 8 | from .addon import print_exc 9 | from . import pme 10 | from .constants import OP_CTX_ITEMS 11 | 12 | 13 | class _XUntokenizer(Untokenizer): 14 | 15 | def compat(self, token, iterable): 16 | indents = [] 17 | toks_append = self.tokens.append 18 | startline = token[0] in (NEWLINE, NL) 19 | prevstring = False 20 | 21 | sp_tup = (NAME, NUMBER) 22 | prevnum = None 23 | for tok in chain([token], iterable): 24 | toknum, tokval = tok[:2] 25 | if toknum == ENCODING: 26 | self.encoding = tokval 27 | prevnum = toknum 28 | continue 29 | 30 | if toknum in sp_tup: 31 | tokval += ' ' 32 | elif toknum == OP and prevnum in sp_tup: 33 | self.tokens[-1] = self.tokens[-1][:-1] 34 | 35 | prevnum = toknum 36 | 37 | if toknum == STRING: 38 | if prevstring: 39 | tokval = ' ' + tokval 40 | prevstring = True 41 | else: 42 | prevstring = False 43 | 44 | if toknum == INDENT: 45 | indents.append(tokval) 46 | continue 47 | elif toknum == DEDENT: 48 | indents.pop() 49 | continue 50 | elif toknum == OP and tokval in ",;": 51 | tokval += " " 52 | elif toknum in (NEWLINE, NL): 53 | startline = True 54 | elif startline and indents: 55 | toks_append(indents[-1]) 56 | startline = False 57 | toks_append(tokval) 58 | 59 | 60 | def _untokenize(stm): 61 | return _XUntokenizer().untokenize(stm) 62 | 63 | 64 | def _join_statements(stms, encoding): 65 | ret = [(ENCODING, encoding)] 66 | 67 | for i, stm in enumerate(stms): 68 | if i > 0: 69 | ret.append((OP, ";")) 70 | 71 | for tok in stm: 72 | ret.append(tok) 73 | 74 | ret.append((ENDMARKER, "")) 75 | return ret 76 | 77 | 78 | def _split_statement(text): 79 | text = text.strip(" ;") 80 | 81 | stms = [] 82 | stm = [] 83 | stms.append(stm) 84 | encoding = None 85 | try: 86 | g = tokenize(BytesIO(text.encode('utf-8')).readline) 87 | encoding = None 88 | for tp, value, _, _, _ in g: 89 | if tp == ENDMARKER: 90 | continue 91 | if tp == ENCODING: 92 | encoding = value 93 | continue 94 | if tp == OP and value == ";": 95 | stm = [] 96 | stms.append(stm) 97 | else: 98 | stm.append((tp, value)) 99 | except: 100 | print_exc() 101 | return [], None 102 | 103 | return stms, encoding 104 | 105 | 106 | def _is_operator(stm): 107 | idx = -1 108 | n = len(stm) 109 | 110 | if n > 0 and stm[0][1] == "O": 111 | idx = 2 112 | elif n > 2 and stm[0][1] == "bpy" and stm[2][1] == "ops": 113 | idx = 4 114 | 115 | if idx + 3 >= n or stm[idx + 3][0] != OP or stm[idx + 3][1] != "(": 116 | return -1 117 | 118 | d = 1 119 | for i in range(idx + 4, n): 120 | if stm[i][0] == NEWLINE: 121 | continue 122 | 123 | if d == 0: 124 | return -1 125 | 126 | if stm[i][1] == "(": 127 | d += 1 128 | elif stm[i][1] == ")": 129 | d -= 1 130 | 131 | return idx 132 | 133 | 134 | def _extract_args(stm, idx, encoding): 135 | args = [] 136 | arg = None 137 | depth = 0 138 | equal = False 139 | has_pos_args = False 140 | pos_args = [] 141 | for i in range(idx, len(stm)): 142 | tp, value = stm[i] 143 | 144 | if tp == OP: 145 | if value in "([{": 146 | depth += 1 147 | if depth == 1: 148 | equal = False 149 | arg = [(ENCODING, encoding)] 150 | args.append(arg) 151 | if value == "(": 152 | continue 153 | 154 | elif value in ")]}": 155 | depth -= 1 156 | if depth == 0: 157 | if len(arg) == 1: 158 | args.pop() 159 | 160 | has_pos_args = has_pos_args or \ 161 | len(args) > 0 and not equal 162 | if args and not equal: 163 | pos_args.append(args.pop()) 164 | 165 | arg = None 166 | 167 | if depth == 1: 168 | if value == "=": 169 | equal = True 170 | 171 | if value == ",": 172 | if args and not equal: 173 | pos_args.append(args.pop()) 174 | arg = [(ENCODING, encoding)] 175 | args.append(arg) 176 | has_pos_args = has_pos_args or not equal 177 | equal = False 178 | else: 179 | arg.append(stm[i]) 180 | else: 181 | if arg: 182 | arg.append(stm[i]) 183 | 184 | else: 185 | if arg: 186 | arg.append(stm[i]) 187 | 188 | return args, pos_args 189 | 190 | 191 | def to_bl_idname(idname): 192 | tp = getattr(bpy.types, idname, None) 193 | if tp and hasattr(tp, "bl_idname"): 194 | return getattr(tp, "bl_idname") 195 | 196 | return idname.lower().replace("_ot_", ".") 197 | 198 | 199 | def to_idname(bl_idname): 200 | op = operator(bl_idname) 201 | if op: 202 | return op.idname() 203 | 204 | return None 205 | 206 | 207 | def operator(bl_idname): 208 | try: 209 | ret = eval("bpy.ops.%s" % bl_idname) 210 | get_rna_type(ret) 211 | except: 212 | ret = None 213 | 214 | return ret 215 | 216 | 217 | def operator_label(value): 218 | if isinstance(value, str): 219 | if "." not in value: 220 | value = to_bl_idname(value) 221 | 222 | op = operator(value) 223 | value = op and get_rna_type(op) 224 | 225 | if not value: 226 | return "" 227 | 228 | label = value.bl_rna.name 229 | if not label: 230 | label = value.bl_rna.identifier 231 | if "_OT_" in label: 232 | label = label.split("_OT_")[-1] 233 | label = label.replace("_", " ").title() 234 | 235 | return label 236 | 237 | 238 | def add_default_args(text): 239 | stms, encoding = _split_statement(text) 240 | 241 | if len(stms) != 1: 242 | return text 243 | 244 | stm = stms[0] 245 | idx = _is_operator(stm) 246 | if idx == -1: 247 | return text 248 | 249 | sargs, spos_args = _extract_args(stm, idx, encoding) 250 | 251 | idx += 4 252 | if not spos_args: 253 | default_args = [ 254 | (STRING, "'INVOKE_DEFAULT'"), 255 | (OP, ","), 256 | (NAME, "True"), 257 | ] 258 | if sargs: 259 | default_args.append((OP, ",")) 260 | 261 | stm[idx:idx] = default_args 262 | 263 | stm = _join_statements(stms, encoding) 264 | 265 | return _untokenize(stm) 266 | 267 | 268 | def legacy_parse_pos_args(pos_args): 269 | # ctx = None 270 | exec_ctx = 'INVOKE_DEFAULT' 271 | undo = True 272 | 273 | if pos_args: 274 | for a in pos_args: 275 | a = eval(a) 276 | if isinstance(a, str): 277 | exec_ctx = a 278 | elif isinstance(a, dict): 279 | # ctx = a 280 | pass 281 | elif isinstance(a, bool): 282 | undo = a 283 | return exec_ctx, undo 284 | 285 | 286 | def _op_ctx_whitelist() -> set[str]: 287 | """Returns a set of valid context mode strings.""" 288 | return {item[0] for item in OP_CTX_ITEMS} 289 | 290 | 291 | def _parse_pos_args(pos_args: list[str]) -> tuple[str, bool]: 292 | """Parses operator positional args into (C_exec, C_undo) tuple.""" 293 | C_exec = 'INVOKE_DEFAULT' 294 | C_undo = True 295 | count = 0 296 | 297 | if pos_args is None: 298 | return C_exec, C_undo 299 | 300 | valid_modes = _op_ctx_whitelist() 301 | 302 | for arg in pos_args: 303 | arg = arg.strip() 304 | 305 | if arg.startswith("{"): 306 | continue 307 | 308 | if arg in ("True", "False"): 309 | C_undo = (arg == "True") 310 | count += 1 311 | else: 312 | trimmed = arg.strip('"\'') 313 | if trimmed in valid_modes: 314 | C_exec = trimmed 315 | count += 1 316 | 317 | if count >= 2: 318 | break 319 | 320 | return C_exec, C_undo 321 | 322 | 323 | def parse_pos_args(pos_args: list[str]) -> tuple[str, bool]: # Issue#25 324 | # TODO: If no errors are reported, it will be removed in the next release. 325 | DBG = False 326 | if not DBG: 327 | return _parse_pos_args(pos_args) 328 | 329 | lg_exec, lg_undo = legacy_parse_pos_args(pos_args) 330 | C_exec, C_undo = _parse_pos_args(pos_args) 331 | 332 | print(""" 333 | parse_pos_args: 334 | Legacy: 335 | C_exec: {0} (type: {1}) 336 | C_undo: {2} (type: {3}) 337 | Current: 338 | C_exec: {4} (type: {5}) 339 | C_undo: {6} (type: {7}) 340 | """.format( 341 | lg_exec, type(lg_exec), 342 | lg_undo, type(lg_undo), 343 | C_exec, type(C_exec), 344 | C_undo, type(C_undo) 345 | )) 346 | 347 | return C_exec, C_undo 348 | 349 | 350 | def find_operator(text): 351 | stms, encoding = _split_statement(text) 352 | 353 | if len(stms) != 1: 354 | return None, None, None 355 | 356 | s = stms[0] 357 | idx = _is_operator(s) 358 | if idx == -1: 359 | return None, None, None 360 | 361 | if s[idx][0] != NAME or s[idx + 1][0] != OP or s[idx + 2][0] != NAME: 362 | return None, None, None 363 | 364 | bl_idname = "%s.%s" % (s[idx][1], s[idx + 2][1]) 365 | 366 | sargs, spos_args = _extract_args(s, idx, encoding) 367 | 368 | args = [] 369 | for sarg in sargs: 370 | args.append(_untokenize(sarg).strip()) 371 | 372 | if len(args) == 1 and not args[0]: 373 | args.pop() 374 | 375 | pos_args = [] 376 | for sarg in spos_args: 377 | pos_args.append(_untokenize(sarg).strip()) 378 | 379 | if len(pos_args) == 1 and not pos_args[0]: 380 | pos_args.pop() 381 | 382 | return bl_idname, args, pos_args 383 | 384 | 385 | def find_statement(text): 386 | stms, encoding = _split_statement(text) 387 | 388 | if len(stms) != 1: 389 | return None, None 390 | 391 | stm = stms[0] 392 | depth = 0 393 | 394 | prop = "" 395 | prop_value = None 396 | for tp, value in stm: 397 | if tp == OP: 398 | if value in "([{": 399 | depth += 1 400 | 401 | elif value in ")]}": 402 | depth -= 1 403 | 404 | elif value == "=" and depth == 0: 405 | prop_value = "" 406 | continue 407 | 408 | elif value == ".": 409 | pass 410 | 411 | # else: 412 | # return None, None 413 | 414 | if prop_value is None: 415 | prop += value 416 | else: 417 | prop_value += value 418 | 419 | if prop_value is None: 420 | prop = None 421 | 422 | return prop, prop_value 423 | 424 | 425 | def _apply_properties(dct, key, value): 426 | if isinstance(value, dict): 427 | if key not in dct: 428 | dct[key] = dict() 429 | 430 | d = getattr(dct, key, None) 431 | if d is None: 432 | return None 433 | 434 | for k, v in value.items(): 435 | _apply_properties(d, k, v) 436 | else: 437 | if hasattr(dct, key): 438 | try: 439 | setattr(dct, key, value) 440 | except: 441 | pass 442 | 443 | 444 | def apply_properties(bl_rna_props, args, pm=None, pmi=None): 445 | exec_globals = pme.context.gen_globals() 446 | exec_globals.update(menu=pm.name, slot=pmi.name) 447 | for arg in args: 448 | key, _, value = arg.partition("=") 449 | key = key.strip() 450 | value = pme.context.eval(value.strip(), exec_globals) 451 | _apply_properties(bl_rna_props, key, value) 452 | 453 | 454 | def compare_operators(o1, o2): 455 | return o1 and o2 and o1.as_pointer() == o2.as_pointer() 456 | 457 | 458 | def get_rna_type(op): 459 | if isinstance(op, str): 460 | try: 461 | op = eval("bpy.ops.%s" % op) 462 | except: 463 | return None 464 | 465 | if hasattr(op, "get_rna"): 466 | return op.get_rna().rna_type 467 | else: 468 | return op.get_rna_type() 469 | -------------------------------------------------------------------------------- /overlay.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import blf 3 | import bgl 4 | from time import time 5 | from .addon import ADDON_ID, prefs, uprefs, ic, is_28 6 | from .utils import multiton 7 | from .layout_helper import split 8 | from . import pme 9 | from . import constants as CC 10 | 11 | OVERLAY_ALIGNMENT_ITEMS = ( 12 | ('TOP', "Top", ""), 13 | ('TOP_LEFT', "Top Left", ""), 14 | ('TOP_RIGHT', "Top Right", ""), 15 | ('BOTTOM', "Bottom", ""), 16 | ('BOTTOM_LEFT', "Bottom Left", ""), 17 | ('BOTTOM_RIGHT', "Bottom Right", ""), 18 | ) 19 | 20 | 21 | def blf_color(r, g, b, a): 22 | if is_28(): 23 | blf.color(0, r, g, b, a) 24 | else: 25 | bgl.glColor4f(r, g, b, a) 26 | 27 | 28 | class Timer: 29 | def __init__(self, t): 30 | self.reset(t) 31 | 32 | def update(self): 33 | t1 = time() 34 | self.t -= t1 - self.t0 35 | self.t0 = t1 36 | 37 | return self.t <= 0 38 | 39 | def reset(self, t): 40 | self.t = t 41 | self.t0 = time() 42 | 43 | def finished(self): 44 | return self.t <= 0 45 | 46 | 47 | class SpaceGroup: 48 | def __init__(self, bl_type): 49 | self.type = bl_type 50 | self.handler = None 51 | self.bl_timer = None 52 | self.timer = Timer(1) 53 | self.text = None 54 | self.data = None 55 | self.alignment = 'TOP' 56 | self.offset_x = 10 57 | self.offset_y = 10 58 | self.shadow = True 59 | 60 | 61 | space_groups = dict() 62 | 63 | 64 | def add_space_group(id, tp_name): 65 | tp = getattr(bpy.types, tp_name, None) 66 | if not tp: 67 | return 68 | 69 | space_groups[id] = SpaceGroup(tp) 70 | 71 | 72 | add_space_group("CLIP_EDITOR", "SpaceClipEditor") 73 | add_space_group("CONSOLE", "SpaceConsole") 74 | add_space_group("DOPESHEET_EDITOR", "SpaceDopeSheetEditor") 75 | add_space_group("FILE_BROWSER", "SpaceFileBrowser") 76 | add_space_group("GRAPH_EDITOR", "SpaceGraphEditor") 77 | add_space_group("IMAGE_EDITOR", "SpaceImageEditor") 78 | add_space_group("INFO", "SpaceInfo") 79 | add_space_group("LOGIC_EDITOR", "SpaceLogicEditor") 80 | add_space_group("NLA_EDITOR", "SpaceNLA") 81 | add_space_group("NODE_EDITOR", "SpaceNodeEditor") 82 | add_space_group("OUTLINER", "SpaceOutliner") 83 | add_space_group("PROPERTIES", "SpaceProperties") 84 | add_space_group("SEQUENCE_EDITOR", "SpaceSequenceEditor") 85 | add_space_group("TEXT_EDITOR", "SpaceTextEditor") 86 | add_space_group("TIMELINE", "SpaceTimeline") 87 | add_space_group(CC.UPREFS, "Space" + CC.UPREFS_CLS) 88 | add_space_group("VIEW_3D", "SpaceView3D") 89 | 90 | del add_space_group 91 | 92 | 93 | _line_y = 0 94 | 95 | 96 | def _draw_line(space, r, g, b, a): 97 | ctx = bpy.context 98 | blf.size(0, space.size) 99 | w, h = blf.dimensions(0, space.text) 100 | 101 | global _line_y 102 | 103 | if "LEFT" in space.alignment: 104 | x = space.offset_x 105 | elif "RIGHT" in space.alignment: 106 | x = ctx.region.width - w - space.offset_x 107 | else: 108 | x = 0.5 * ctx.region.width - 0.5 * w 109 | 110 | if "TOP" in space.alignment: 111 | _line_y += space.size + 3 112 | y = ctx.region.height - _line_y - space.offset_y 113 | else: 114 | y = _line_y + space.offset_y 115 | _line_y += space.size + 3 116 | 117 | blf.position(0, x, y, 0) 118 | blf_color(r, g, b, a) 119 | blf.draw(0, space.text) 120 | 121 | 122 | def _draw_handler(space): 123 | r, g, b, a = space.color 124 | p = 1 if space.timer.t >= 0.3 else space.timer.t / 0.3 125 | 126 | if space.shadow: 127 | blf.enable(0, blf.SHADOW) 128 | blf.shadow_offset(0, 1, -1) 129 | blf.shadow(0, 5, 0.0, 0.0, 0.0, a * 0.4 * p) 130 | 131 | global _line_y 132 | _line_y = 0 133 | if space.text: 134 | _draw_line(space, r, g, b, a * p) 135 | 136 | blf.disable(0, blf.SHADOW) 137 | 138 | 139 | class Painter: 140 | def __init__(self): 141 | self.overlay = None 142 | 143 | 144 | class Style: 145 | def __init__(self, color=None, size=None): 146 | self.color = color or (1, 1, 1, 1) 147 | self.size = size or 30 148 | 149 | def update(self, color=None, size=None): 150 | if color: 151 | self.color = color 152 | if size: 153 | self.size = size 154 | 155 | 156 | class Text: 157 | default_style = Style() 158 | secondary_style = Style() 159 | 160 | def __init__(self, text, style=None, size=0): 161 | self.style = style if style else self.default_style 162 | self._size = size 163 | self.update(text) 164 | 165 | @property 166 | def size(self): 167 | return self._size or self.style.size 168 | 169 | def center(self, width): 170 | return int(0.5 * (width - self.width)) 171 | 172 | def right(self, width): 173 | return width - self.width 174 | 175 | def update(self, text): 176 | self.text = text 177 | blf.size(0, self.size) 178 | self.width, self.height = blf.dimensions(0, text) 179 | 180 | def draw(self, x, y): 181 | blf_color(*self.style.color) 182 | blf.position(0, x, y, 0) 183 | blf.size(0, self.size) 184 | blf.draw(0, self.text) 185 | 186 | 187 | class Col: 188 | def __init__(self): 189 | self.width = 0 190 | self.cells = [] 191 | 192 | def add_cell(self, text, style, size): 193 | self.cells.append(Text(text, style, size)) 194 | 195 | def init(self): 196 | self.width = 0 197 | for cell in self.cells: 198 | self.width = max(self.width, cell.width) 199 | 200 | 201 | class TablePainter(Painter): 202 | spacing_x = 8 203 | spacing_y = 1 204 | spacing_h = 5 205 | line_width = 2 206 | col_style_scale = 0.7 207 | col_styles = (Text.secondary_style, Text.default_style) 208 | 209 | def __init__(self, num_cols, data, header=None, align_right=1): 210 | Painter.__init__(self) 211 | 212 | self.cols = [] 213 | self.num_cols = num_cols 214 | self.header = Text(header) if header else None 215 | self.align_right = align_right 216 | 217 | self.update(data) 218 | 219 | def update(self, data=None): 220 | pr = prefs().overlay 221 | self.col_size = pr.size * 1 222 | 223 | if data is not None: 224 | self.cols.clear() 225 | 226 | for i in range(0, self.num_cols): 227 | self.cols.append(Col()) 228 | 229 | if isinstance(data, str): 230 | cells = data.split("|") if self.num_cols > 1 else (data,) 231 | else: 232 | cells = data 233 | col_idx = 0 234 | for cell in cells: 235 | col = self.cols[col_idx] 236 | col_style = self.col_styles[col_idx % 2] 237 | col.add_cell( 238 | cell, col_style, 239 | round(col_style.size * self.col_style_scale)) 240 | col_idx = (col_idx + 1) % self.num_cols 241 | 242 | self.width = 0 243 | self.height = 0 244 | if self.header: 245 | self.width = self.header.width 246 | self.height += self.header.height 247 | 248 | width = self.spacing_x * (self.num_cols - 1) 249 | height = 0 250 | for col in self.cols: 251 | col.init() 252 | num_cells = len(col.cells) 253 | width += col.width 254 | height = max( 255 | height, 256 | self.col_size * num_cells + self.spacing_y * (num_cells - 1)) 257 | self.width = max(self.width, width) 258 | if height: 259 | if self.header: 260 | self.height += self.spacing_y 261 | self.height += height 262 | 263 | r = bpy.context.region 264 | 265 | if 'LEFT' in pr.alignment: 266 | self.x = pr.offset_x 267 | elif 'RIGHT' in pr.alignment: 268 | self.x = r.width - self.width - pr.offset_x 269 | else: 270 | self.x = 0.5 * r.width - 0.5 * self.width 271 | 272 | if 'TOP' in pr.alignment: 273 | self.y = r.height - pr.offset_y 274 | else: 275 | self.y = pr.offset_y + self.height 276 | 277 | def draw(self): 278 | if self.header: 279 | x = round(self.x + self.header.center(self.width)) 280 | y = round(self.y - self.header.size) 281 | self.header.draw(x, y) 282 | 283 | if not is_28(): 284 | bgl.glLineWidth(self.line_width) 285 | blf_color(*self.header.style.color) 286 | bgl.glBegin(bgl.GL_LINES) 287 | bgl.glVertex2f(self.x, y - self.spacing_h - self.line_width) 288 | bgl.glVertex2f( 289 | self.x + self.width, y - self.spacing_h - self.line_width) 290 | bgl.glEnd() 291 | 292 | x = 0 293 | for i in range(0, self.num_cols - self.align_right): 294 | col = self.cols[i] 295 | y = -self.header.size - self.spacing_y - 2 * self.spacing_h - \ 296 | self.line_width if self.header else 0 297 | for cell in col.cells: 298 | cell.draw(self.x + x, self.y + y - self.col_size) 299 | y -= self.col_size + self.spacing_y 300 | x += col.width + self.spacing_x 301 | 302 | x = self.width 303 | for i in range(0, self.align_right): 304 | col = self.cols[self.num_cols - i - 1] 305 | y = -self.header.size - self.spacing_y - 2 * self.spacing_h - \ 306 | self.line_width if self.header else 0 307 | for cell in col.cells: 308 | cell.draw(self.x + x - cell.width, self.y + y - self.col_size) 309 | y -= self.col_size + self.spacing_y 310 | x -= col.width + self.spacing_x 311 | 312 | 313 | @multiton 314 | class Overlay: 315 | def __init__(self, id=None): 316 | self.id = id or bpy.context.area.type 317 | self.space = type(bpy.context.space_data) 318 | self.handler = None 319 | 320 | self.painters = [] 321 | self.win_area = None 322 | 323 | self.alpha = 1 324 | 325 | def add_painter(self, painter): 326 | if painter.overlay: 327 | return 328 | painter.overlay = self 329 | self.painters.append(painter) 330 | 331 | @staticmethod 332 | def draw(self): 333 | pr = prefs().overlay 334 | 335 | if pr.shadow: 336 | a = 1 337 | blf.enable(0, blf.SHADOW) 338 | blf.shadow_offset(0, 1, -1) 339 | blf.shadow(0, 5, 0.0, 0.0, 0.0, a * 0.4 * self.alpha) 340 | 341 | for p in self.painters: 342 | p.draw() 343 | 344 | if pr.shadow: 345 | blf.disable(0, blf.SHADOW) 346 | 347 | def show(self): 348 | if self.handler: 349 | return 350 | self.handler = self.space.draw_handler_add( 351 | self.__class__.draw, (self,), 'WINDOW', 'POST_PIXEL') 352 | 353 | self.win_area = bpy.context.area 354 | # for r in bpy.context.area.regions: 355 | # if r.type == 'WINDOW': 356 | # self.win_area = r 357 | # break 358 | 359 | self.tag_redraw() 360 | 361 | def hide(self): 362 | if self.handler: 363 | self.space.draw_handler_remove(self.handler, 'WINDOW') 364 | self.handler = None 365 | 366 | self.painters.clear() 367 | self.tag_redraw() 368 | self.win_area = None 369 | 370 | def tag_redraw(self): 371 | if self.win_area: 372 | self.win_area.tag_redraw() 373 | 374 | 375 | class OverlayPrefs(bpy.types.PropertyGroup): 376 | def size_update(self, context): 377 | Text.default_style.size = self.size 378 | Text.secondary_style.size = self.size 379 | # TablePainter.col_styles[0].size = \ 380 | # round(self.size * TablePainter.col_style_scale) 381 | # TablePainter.col_styles[1].size = \ 382 | # round(self.size * TablePainter.col_style_scale) 383 | 384 | TablePainter.line_width = 1 if self.size < 18 else 2 385 | 386 | def color_update(self, context): 387 | Text.default_style.update(list(self.color)) 388 | 389 | def color2_update(self, context): 390 | Text.secondary_style.update(list(self.color2)) 391 | 392 | overlay: bpy.props.BoolProperty( 393 | name="Use Overlay", 394 | description="Use overlay for stack keys and modal operators", 395 | default=True) 396 | size: bpy.props.IntProperty( 397 | name="Font Size", description="Font size", 398 | default=24, min=10, max=50, options={'SKIP_SAVE'}, 399 | update=size_update) 400 | color: bpy.props.FloatVectorProperty( 401 | name="Color", description="Color", 402 | default=(1, 1, 1, 1), subtype='COLOR', size=4, min=0, max=1, 403 | update=color_update) 404 | color2: bpy.props.FloatVectorProperty( 405 | name="Color", description="Color", 406 | default=(1, 1, 0, 1), subtype='COLOR', size=4, min=0, max=1, 407 | update=color2_update) 408 | alignment: bpy.props.EnumProperty( 409 | name="Alignment", 410 | description="Alignment", 411 | items=OVERLAY_ALIGNMENT_ITEMS, 412 | default='TOP') 413 | duration: bpy.props.FloatProperty( 414 | name="Duration", subtype='TIME', min=1, max=10, default=2, step=10) 415 | offset_x: bpy.props.IntProperty( 416 | name="Offset X", description="Offset from area edges", 417 | subtype='PIXEL', default=10, min=0) 418 | offset_y: bpy.props.IntProperty( 419 | name="Offset Y", description="Offset from area edges", 420 | subtype='PIXEL', default=10, min=0) 421 | shadow: bpy.props.BoolProperty( 422 | name="Use Shadow", description="Use shadow", default=True) 423 | 424 | def draw(self, layout): 425 | # if not self.overlay: 426 | # layout.prop(self, "overlay", toggle=True) 427 | # else: 428 | # layout.prop(self, "overlay") 429 | 430 | col = layout.column(align=True) 431 | col.active = self.overlay 432 | 433 | row = split(col, 0.5, True) 434 | row1 = row.row(align=True) 435 | row1.prop(self, "color", text="") 436 | row1.prop(self, "color2", text="") 437 | row1.prop(self, "shadow", text="", icon=ic('META_BALL')) 438 | 439 | row.prop(self, "size") 440 | row.prop(self, "duration") 441 | 442 | row = split(col, 0.5, True) 443 | row.prop(self, "alignment", text="") 444 | row.prop(self, "offset_x") 445 | row.prop(self, "offset_y") 446 | 447 | 448 | class PME_OT_overlay(bpy.types.Operator): 449 | bl_idname = "pme.overlay" 450 | bl_label = "" 451 | bl_options = {'INTERNAL'} 452 | 453 | is_running = False 454 | 455 | text: bpy.props.StringProperty(options={'SKIP_SAVE'}) 456 | alignment: bpy.props.EnumProperty( 457 | name="Alignment", 458 | description="Alignment", 459 | items=OVERLAY_ALIGNMENT_ITEMS, 460 | default='TOP', options={'SKIP_SAVE'}) 461 | duration: bpy.props.FloatProperty( 462 | name="Duration", subtype='TIME', min=1, default=2, step=10, 463 | options={'SKIP_SAVE'}) 464 | offset_x: bpy.props.IntProperty( 465 | name="Offset X", description="Offset from area edges", 466 | subtype='PIXEL', default=10, min=0, options={'SKIP_SAVE'}) 467 | offset_y: bpy.props.IntProperty( 468 | name="Offset Y", description="Offset from area edges", 469 | subtype='PIXEL', default=10, min=0, options={'SKIP_SAVE'}) 470 | 471 | def modal(self, context, event): 472 | if event.type == 'TIMER': 473 | num_handlers = 0 474 | active_areas = set() 475 | for name, space in space_groups.items(): 476 | if not space.handler: 477 | continue 478 | 479 | active_areas.add(name) 480 | 481 | if space.timer.update(): 482 | space.type.draw_handler_remove( 483 | space.handler, 'WINDOW') 484 | space.handler = None 485 | else: 486 | num_handlers += 1 487 | 488 | for area in context.screen.areas: 489 | if area.type in active_areas: 490 | area.tag_redraw() 491 | 492 | if not num_handlers: 493 | context.window_manager.event_timer_remove(self.timer) 494 | self.timer = None 495 | PME_OT_overlay.is_running = False 496 | return {'FINISHED'} 497 | 498 | return {'PASS_THROUGH'} 499 | 500 | def execute(self, context): 501 | if context.area.type not in space_groups: 502 | return {'CANCELLED'} 503 | 504 | pr = uprefs().addons[ADDON_ID].preferences 505 | 506 | # if not pr.overlay.overlay: 507 | # if not hasattr(bgl, "glColor4f"): 508 | # return {'CANCELLED'} 509 | 510 | space = space_groups[context.area.type] 511 | space.timer.reset( 512 | self.duration if "duration" in self.properties 513 | else pr.overlay.duration) 514 | space.text = self.text 515 | space.size = pr.overlay.size 516 | space.alignment = self.alignment if "alignment" in self.properties \ 517 | else pr.overlay.alignment 518 | space.offset_x = self.offset_x if "offset_x" in self.properties \ 519 | else pr.overlay.offset_x 520 | space.offset_y = self.offset_y if "offset_y" in self.properties \ 521 | else pr.overlay.offset_y 522 | space.shadow = pr.overlay.shadow 523 | space.color = list(pr.overlay.color) 524 | 525 | if space.handler: 526 | return {'CANCELLED'} 527 | 528 | space.handler = space.type.draw_handler_add( 529 | _draw_handler, (space,), 'WINDOW', 'POST_PIXEL') 530 | 531 | if not PME_OT_overlay.is_running: 532 | PME_OT_overlay.is_running = True 533 | context.window_manager.modal_handler_add(self) 534 | self.timer = context.window_manager.event_timer_add( 535 | 0.1, window=bpy.context.window) 536 | 537 | return {'RUNNING_MODAL'} 538 | 539 | 540 | def overlay(text, **kwargs): 541 | bpy.ops.pme.overlay(text=text, **kwargs) 542 | return True 543 | 544 | 545 | def register(): 546 | opr = prefs().overlay 547 | Text.default_style.update(list(opr.color), opr.size) 548 | Text.secondary_style.update(list(opr.color2), opr.size) 549 | # TablePainter.col_styles[0].update( 550 | # list(opr.color2), round(opr.size * TablePainter.col_style_scale)) 551 | # TablePainter.col_styles[1].update( 552 | # list(opr.color), round(opr.size * TablePainter.col_style_scale)) 553 | 554 | pme.context.add_global("overlay", overlay) 555 | -------------------------------------------------------------------------------- /pme.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .addon import prefs, temp_prefs, print_exc 3 | 4 | 5 | class UserData: 6 | def get(self, name, default=None): 7 | return self.__dict__.get(name, default) 8 | 9 | def update(self, **kwargs): 10 | self.__dict__.update(**kwargs) 11 | 12 | def __getattr__(self, name): 13 | return self.__dict__.get(name, None) 14 | 15 | 16 | class PMEContext: 17 | 18 | def __init__(self): 19 | self._globals = dict( 20 | bpy=bpy, 21 | pme_context=self, 22 | drag_x=0, 23 | drag_y=0, 24 | ) 25 | self.pm = None 26 | self.pmi = None 27 | self.index = None 28 | self.icon = None 29 | self.icon_value = None 30 | self.text = None 31 | self.region = None 32 | self.last_operator = None 33 | self.is_first_draw = True 34 | self.exec_globals = None 35 | self.exec_locals = None 36 | self.exec_user_locals = dict() 37 | self._layout = None 38 | self._event = None 39 | self.edit_item_idx = None 40 | 41 | def __getattr__(self, name): 42 | return self._globals.get(name, None) 43 | 44 | def item_id(self): 45 | pmi = self.pmi 46 | id = self.pm.name 47 | id += pmi.name if pmi.name else pmi.text 48 | id += str(self.index) 49 | return id 50 | 51 | def reset(self): 52 | self.is_first_draw = True 53 | self.exec_globals = None 54 | self.exec_locals = None 55 | 56 | def add_global(self, key, value): 57 | self._globals[key] = value 58 | 59 | @property 60 | def layout(self): 61 | return self._layout 62 | 63 | @layout.setter 64 | def layout(self, value): 65 | self._layout = value 66 | self._globals["L"] = value 67 | 68 | @property 69 | def event(self): 70 | return self._event 71 | 72 | @event.setter 73 | def event(self, value): 74 | self._event = value 75 | self._globals["E"] = value 76 | 77 | if self._event: 78 | if self._event.type == 'WHEELUPMOUSE': 79 | self._globals["delta"] = 1 80 | elif self._event.type == 'WHEELDOWNMOUSE': 81 | self._globals["delta"] = -1 82 | 83 | @property 84 | def globals(self): 85 | if self._globals["D"].__class__.__name__ == "_RestrictData": 86 | # self._globals["C"] = bpy.context 87 | self._globals["D"] = bpy.data 88 | return self._globals 89 | 90 | def gen_globals(self, **kwargs): 91 | ret = dict( 92 | text=self.text, 93 | icon=self.icon, 94 | icon_value=self.icon_value, 95 | PME=temp_prefs(), 96 | PREFS=prefs(), 97 | **kwargs 98 | ) 99 | 100 | ret.update(self.exec_user_locals) 101 | ret.update(self.globals) 102 | 103 | return ret 104 | 105 | def eval(self, expression, globals=None, menu=None, slot=None): 106 | if globals is None: 107 | globals = self.gen_globals() 108 | 109 | # globals["menu"] = menu 110 | # globals["slot"] = slot 111 | 112 | value = None 113 | try: 114 | value = eval(expression, globals) 115 | except: 116 | print_exc(expression) 117 | 118 | return value 119 | 120 | def exe(self, data, globals=None, menu=None, slot=None, use_try=True): 121 | if globals is None: 122 | globals = self.gen_globals() 123 | 124 | # globals["menu"] = menu 125 | # globals["slot"] = slot 126 | 127 | if not use_try: 128 | exec(data, globals) 129 | return True 130 | 131 | try: 132 | exec(data, globals) 133 | except: 134 | print_exc(data) 135 | return False 136 | 137 | return True 138 | 139 | 140 | context = PMEContext() 141 | 142 | 143 | class PMEProp: 144 | def __init__(self, type, name, default, ptype='STR', items=None): 145 | self.name = name 146 | self.default = default 147 | self.items = items 148 | self.type = type 149 | self.ptype = ptype 150 | 151 | def decode_value(self, value): 152 | if self.ptype == 'STR': 153 | return value 154 | elif self.ptype == 'BOOL': 155 | return value == "True" or value == "1" 156 | elif self.ptype == 'INT': 157 | return int(value) if value else 0 158 | 159 | 160 | class PMEProps: 161 | prop_map = {} 162 | 163 | def IntProperty(self, type, name, default=0): 164 | # default = "" if default == 0 else str(default) 165 | self.prop_map[name] = PMEProp(type, name, default, 'INT') 166 | 167 | def BoolProperty(self, type, name, default=False): 168 | # default = "1" if default else "" 169 | self.prop_map[name] = PMEProp(type, name, default, 'BOOL') 170 | 171 | def StringProperty(self, type, name, default=""): 172 | self.prop_map[name] = PMEProp(type, name, default, 'STR') 173 | 174 | def EnumProperty(self, type, name, default, items): 175 | self.prop_map[name] = PMEProp(type, name, default, 'STR', items) 176 | 177 | def __init__(self): 178 | self.parsed_data = {} 179 | 180 | def get(self, name): 181 | return self.prop_map.get(name, None) 182 | 183 | def parse(self, text): 184 | if text not in self.parsed_data: 185 | self.parsed_data[text] = ParsedData(text) 186 | 187 | return self.parsed_data[text] 188 | 189 | def encode(self, text, prop, value): 190 | tp, _, data = text.partition("?") 191 | 192 | data = data.split("&") 193 | lst = [] 194 | has_prop = False 195 | for pr in data: 196 | if not pr: 197 | continue 198 | 199 | k, v = pr.split("=") 200 | if k not in props.prop_map: 201 | continue 202 | 203 | if k == prop: 204 | # v = props.prop_map[k].decode_value(value) 205 | v = value 206 | has_prop = True 207 | 208 | if v != props.get(k).default: 209 | lst.append("%s=%s" % (k, v)) 210 | 211 | if not has_prop and value != props.prop_map[prop].default: 212 | lst.append("%s=%s" % (prop, value)) 213 | 214 | lst.sort() 215 | 216 | text = "%s?%s" % (tp, "&".join(lst)) 217 | return text 218 | 219 | def clear(self, text, *args): 220 | tp, _, data = text.partition("?") 221 | 222 | data = data.split("&") 223 | lst = [] 224 | for pr in data: 225 | if not pr: 226 | continue 227 | 228 | k, v = pr.split("=") 229 | if k not in props.prop_map or k in args: 230 | continue 231 | 232 | if v != props.get(k).default: 233 | lst.append(pr) 234 | 235 | lst.sort() 236 | 237 | text = "%s?%s" % (tp, "&".join(lst)) 238 | return text 239 | 240 | 241 | props = PMEProps() 242 | 243 | 244 | class ParsedData: 245 | 246 | def __init__(self, text): 247 | self.type, _, data = text.partition("?") 248 | 249 | for k, prop in props.prop_map.items(): 250 | if prop.type == self.type: 251 | setattr(self, k, prop.default) 252 | 253 | data = data.split("&") 254 | for prop in data: 255 | if not prop: 256 | continue 257 | k, v = prop.split("=") 258 | if k in props.prop_map: 259 | setattr(self, k, props.prop_map[k].decode_value(v)) 260 | 261 | self.is_empty = True 262 | for k, prop in props.prop_map.items(): 263 | if not hasattr(self, k): 264 | continue 265 | if getattr(self, k) != prop.default: 266 | self.is_empty = False 267 | break 268 | 269 | def value(self, name): 270 | for item in props.get(name).items: 271 | if getattr(self, name) == item[0]: 272 | return item[2] 273 | 274 | return 0 275 | 276 | 277 | def register(): 278 | context.add_global("U", UserData()) 279 | -------------------------------------------------------------------------------- /previews_helper.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bpy.utils.previews 3 | import os 4 | from . import pme 5 | 6 | 7 | class PreviewsHelper: 8 | 9 | def __init__(self, folder="icons"): 10 | self.path = os.path.join(os.path.dirname(__file__), folder) 11 | self.preview = None 12 | 13 | def get_icon(self, name): 14 | return self.preview[name].icon_id 15 | 16 | def get_icon_name_by_id(self, id): 17 | name = None 18 | min_id = 99999999 19 | for k, i in self.preview.items(): 20 | if i.icon_id == id: 21 | return k 22 | if min_id > i.icon_id: 23 | min_id = i.icon_id 24 | name = k 25 | 26 | return name 27 | 28 | def get_names(self): 29 | return self.preview.keys() 30 | 31 | def has_icon(self, name): 32 | return name in self.preview 33 | 34 | def refresh(self): 35 | if self.preview: 36 | self.unregister() 37 | 38 | self.preview = bpy.utils.previews.new() 39 | for f in os.listdir(self.path): 40 | if not f.endswith(".png"): 41 | continue 42 | 43 | self.preview.load( 44 | os.path.splitext(f)[0], 45 | os.path.join(self.path, f), 46 | 'IMAGE') 47 | 48 | def unregister(self): 49 | if not self.preview: 50 | return 51 | bpy.utils.previews.remove(self.preview) 52 | self.preview = None 53 | 54 | 55 | def custom_icon(icon): 56 | return ph.get_icon(icon) 57 | 58 | 59 | if "ph" in globals(): 60 | ph.unregister() 61 | 62 | ph = PreviewsHelper() 63 | ph.refresh() 64 | 65 | 66 | def register(): 67 | pme.context.add_global("custom_icon", custom_icon) 68 | 69 | 70 | def unregister(): 71 | ph.unregister() 72 | -------------------------------------------------------------------------------- /property_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from types import BuiltinFunctionType 3 | from mathutils import Euler 4 | from math import pi as PI 5 | 6 | from .addon import prefs, temp_prefs, print_exc 7 | from . import operator_utils 8 | 9 | bpy.context.window_manager["pme_temp"] = dict() 10 | IDPropertyGroup = type(bpy.context.window_manager["pme_temp"]) 11 | del bpy.context.window_manager["pme_temp"] 12 | 13 | bpy.types.WindowManager.pme_temp = bpy.props.BoolVectorProperty(size=3) 14 | BPyPropArray = type(bpy.context.window_manager.pme_temp) 15 | del bpy.types.WindowManager.pme_temp 16 | 17 | 18 | class PropertyData: 19 | DEFAULT_MIN = -99999 20 | DEFAULT_MAX = 99999 21 | DEFAULT_STEP = 1 22 | 23 | def __init__(self): 24 | self.clear() 25 | 26 | def clear(self): 27 | self.path = None 28 | self.identifier = None 29 | self.rna_prop = None 30 | self.rna_type = None 31 | self.data = None 32 | self.data_path = None 33 | self.threshold = None 34 | self.is_float = False 35 | self.min = self.DEFAULT_MIN 36 | self.max = self.DEFAULT_MAX 37 | self._step = self.DEFAULT_STEP 38 | self.custom = "" 39 | self.icon = None 40 | 41 | tpr = temp_prefs() 42 | if tpr: 43 | tpr.modal_item_prop_step_is_set = False 44 | 45 | @property 46 | def step(self): 47 | if self.rna_prop and \ 48 | self.rna_prop.subtype == 'ANGLE': 49 | return PI * self._step / 180 50 | 51 | return self._step 52 | 53 | def init(self, path, exec_globals, exec_locals=None): 54 | if path == self.path: 55 | return 56 | 57 | if exec_locals is None: 58 | exec_locals = dict() 59 | 60 | pr = prefs() 61 | self.clear() 62 | self.path = path 63 | self.data_path, self.identifier = split_prop_path(path) 64 | value = None 65 | 66 | if self.path: 67 | try: 68 | value = eval(self.path, exec_globals, exec_locals) 69 | except: 70 | pass 71 | # print_exc() 72 | 73 | if self.data_path: 74 | try: 75 | self.data = eval(self.data_path, exec_globals, exec_locals) 76 | except: 77 | print_exc(self.data_path) 78 | 79 | self.rna_prop = rna_prop = get_rna_prop(self.data, self.identifier) 80 | 81 | if rna_prop: 82 | self.rna_type = rna_prop_type = type(rna_prop) 83 | 84 | if value is None: 85 | value = rna_prop.default 86 | 87 | if rna_prop_type == bpy.types.EnumProperty: 88 | self.threshold = pr.get_threshold('ENUM') 89 | 90 | elif rna_prop_type == bpy.types.IntProperty: 91 | self.threshold = pr.get_threshold('INT') 92 | 93 | elif rna_prop_type == bpy.types.FloatProperty: 94 | self.threshold = pr.get_threshold('FLOAT') 95 | self.is_float = True 96 | 97 | elif rna_prop_type == bpy.types.BoolProperty: 98 | self.threshold = pr.get_threshold('BOOL') 99 | 100 | if rna_prop_type == bpy.types.IntProperty or \ 101 | rna_prop_type == bpy.types.FloatProperty: 102 | self.min = rna_prop.soft_min 103 | self.max = rna_prop.soft_max 104 | self._step = rna_prop.step 105 | 106 | if rna_prop_type == bpy.types.FloatProperty: 107 | self._step *= 0.01 108 | if rna_prop.subtype == 'ANGLE': 109 | self._step = 180 * self._step / PI 110 | 111 | else: 112 | self.min = self.DEFAULT_MIN 113 | self.max = self.DEFAULT_MAX 114 | self._step = self.DEFAULT_STEP 115 | 116 | if self.threshold is None: 117 | if value is not None: 118 | value_type = type(value) 119 | if value_type is int: 120 | self.threshold = pr.get_threshold('INT') 121 | self.rna_type = bpy.types.IntProperty 122 | elif value_type is float: 123 | self.threshold = pr.get_threshold('FLOAT') 124 | self.is_float = True 125 | self.rna_type = bpy.types.FloatProperty 126 | elif value_type is bool: 127 | self.threshold = pr.get_threshold('BOOL') 128 | self.rna_type = bpy.types.BoolProperty 129 | else: 130 | self.threshold = pr.get_threshold() 131 | 132 | 133 | class _PGVars: 134 | uid = 0 135 | instances = {} 136 | 137 | @staticmethod 138 | def get(item): 139 | if not item.name: 140 | _PGVars.uid += 1 141 | item.name = str(_PGVars.uid) 142 | 143 | if item.name not in _PGVars.instances: 144 | _PGVars.instances[item.name] = _PGVars() 145 | 146 | return _PGVars.instances[item.name] 147 | 148 | 149 | class DynamicPG(bpy.types.PropertyGroup): 150 | 151 | def getvar(self, var): 152 | pgvars = _PGVars.get(self) 153 | return getattr(pgvars, var) 154 | 155 | def hasvar(self, var): 156 | if not self.name or self.name not in _PGVars.instances: 157 | return False 158 | pgvars = _PGVars.get(self) 159 | return hasattr(pgvars, var) 160 | 161 | def setvar(self, var, value): 162 | pgvars = _PGVars.get(self) 163 | setattr(pgvars, var, value) 164 | return getattr(pgvars, var) 165 | 166 | 167 | def to_dict(obj): 168 | dct = {} 169 | 170 | try: 171 | dct["name"] = obj["name"] 172 | except: 173 | pass 174 | 175 | if not hasattr(obj.__class__, "__annotations__"): 176 | return dct 177 | 178 | pdtype = getattr(bpy.props, "_PropertyDeferred", tuple) 179 | for k in obj.__class__.__annotations__: 180 | pd = obj.__class__.__annotations__[k] 181 | pfunc = getattr(pd, "function", None) or pd[0] 182 | pkeywords = pd.keywords if hasattr(pd, "keywords") else pd[1] 183 | if not isinstance(pd, pdtype) or \ 184 | isinstance(pd, tuple) and len(pd) != 2 or \ 185 | not isinstance(pfunc, BuiltinFunctionType): 186 | continue 187 | 188 | try: 189 | if pfunc is bpy.props.CollectionProperty or \ 190 | pfunc is bpy.props.PointerProperty: 191 | value = getattr(obj, k) 192 | else: 193 | value = obj[k] 194 | except: 195 | if "get" in pkeywords: 196 | continue 197 | 198 | value = getattr(obj, k) 199 | 200 | if pfunc is bpy.props.PointerProperty: 201 | dct[k] = to_dict(value) 202 | 203 | elif pfunc is bpy.props.CollectionProperty: 204 | dct[k] = [] 205 | for item in value.values(): 206 | dct[k].append(to_dict(item)) 207 | 208 | elif isinstance(value, (bool, int, float, str)): 209 | dct[k] = value 210 | 211 | return dct 212 | 213 | 214 | def from_dict(obj, dct): 215 | for k, value in dct.items(): 216 | if isinstance(value, dict): 217 | from_dict(getattr(obj, k), value) 218 | 219 | elif isinstance(value, list): 220 | col = getattr(obj, k) 221 | col.clear() 222 | 223 | for item in value: 224 | from_dict(col.add(), item) 225 | 226 | else: 227 | obj[k] = value 228 | 229 | 230 | def to_py_value(data, key, value): 231 | if isinstance(value, bpy.types.PropertyGroup): 232 | return None 233 | 234 | if isinstance(value, bpy.types.OperatorProperties): 235 | rna_type = operator_utils.get_rna_type( 236 | operator_utils.to_bl_idname(key)) 237 | if not rna_type: 238 | return None 239 | 240 | d = dict() 241 | for k in value.keys(): 242 | py_value = to_py_value(rna_type, k, getattr(value, k)) 243 | if py_value is None or isinstance(py_value, dict) and not py_value: 244 | continue 245 | d[k] = py_value 246 | 247 | return d 248 | 249 | is_bool = isinstance(data.properties[key], bpy.types.BoolProperty) 250 | 251 | if hasattr(value, "to_list"): 252 | value = value.to_list() 253 | if is_bool: 254 | value = [bool(v) for v in value] 255 | elif hasattr(value, "to_tuple"): 256 | value = value.to_tuple() 257 | if is_bool: 258 | value = tuple(bool(v) for v in value) 259 | elif isinstance(value, BPyPropArray): 260 | value = list(value) 261 | if is_bool: 262 | value = [bool(v) for v in value] 263 | elif isinstance(value, Euler): 264 | value = (value.x, value.y, value.z) 265 | 266 | return value 267 | 268 | 269 | def split_prop_path(prop_path): 270 | data_path, _, prop = prop_path.rpartition(".") 271 | return data_path, prop 272 | 273 | 274 | def get_rna_prop(data, prop): 275 | if not data or not prop: 276 | return None 277 | 278 | rna_type = getattr(data, "rna_type", None) 279 | if not rna_type: 280 | return None 281 | 282 | return rna_type.properties[prop] if prop in rna_type.properties else None 283 | 284 | 285 | def is_enum(data, key): 286 | return isinstance(data.bl_rna.properties[key], bpy.types.EnumProperty) 287 | 288 | 289 | def enum_id_to_value(data, key, id): 290 | for item in data.bl_rna.properties[key].enum_items: 291 | if item.identifier == id: 292 | return item.value 293 | return -1 294 | 295 | 296 | def enum_value_to_id(data, key, value): 297 | return data.bl_rna.properties[key].enum_items[value].identifier 298 | -------------------------------------------------------------------------------- /screen_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from dataclasses import dataclass, field 4 | from typing import Any, Dict, Optional, Union 5 | 6 | from . import c_utils as CTU 7 | from . import pme 8 | from .addon import uprefs 9 | # from .bl_utils import ctx_dict 10 | from .debug_utils import logi 11 | 12 | 13 | def redraw_screen(area=None): 14 | # area = area or bpy.context.area or bpy.context.screen.areas[0] 15 | # if not area: 16 | # return 17 | 18 | # with bpy.context.temp_override(area=area): 19 | # bpy.ops.screen.screen_full_area() 20 | 21 | view = uprefs().view 22 | s = view.ui_scale 23 | view.ui_scale = 0.5 24 | view.ui_scale = s 25 | 26 | 27 | def toggle_header(area): 28 | area.spaces.active.show_region_header = \ 29 | not area.spaces.active.show_region_header 30 | 31 | 32 | def move_header(area=None, top=None, visible=None, auto=None): 33 | if top is None and visible is None and auto is None: 34 | return True 35 | 36 | if auto is not None and top is None: 37 | return True 38 | 39 | area = area or bpy.context.area 40 | if not area: 41 | return True 42 | 43 | rh, rw = None, None 44 | for r in area.regions: 45 | if r.type == 'HEADER': 46 | rh = r 47 | elif r.type == 'WINDOW': 48 | rw = r 49 | 50 | is_visible = rh.height > 1 51 | if is_visible: 52 | is_top = rw.y == area.y 53 | else: 54 | is_top = rh.y > area.y 55 | 56 | kwargs = get_override_args(area=area, region=rh) 57 | if auto: 58 | if top: 59 | if is_top: 60 | with bpy.context.temp_override(**kwargs): 61 | toggle_header(area) 62 | else: 63 | with bpy.context.temp_override(**kwargs): 64 | bpy.ops.screen.region_flip() 65 | not is_visible and toggle_header(area) 66 | else: 67 | if is_top: 68 | with bpy.context.temp_override(**kwargs): 69 | bpy.ops.screen.region_flip() 70 | not is_visible and toggle_header(area) 71 | else: 72 | with bpy.context.temp_override(**kwargs): 73 | toggle_header(area) 74 | else: 75 | if top is not None and top != is_top: 76 | with bpy.context.temp_override(**kwargs): 77 | bpy.ops.screen.region_flip() 78 | 79 | if visible is not None and visible != is_visible: 80 | with bpy.context.temp_override(**kwargs): 81 | toggle_header(area) 82 | return True 83 | 84 | 85 | # def parse_extra_keywords(kwargs_str: str) -> dict: 86 | # """ 87 | # Parse a comma-separated string like: 88 | # "window=Window, screen=Screen.001, workspace=MyWorkspace" 89 | # into a dict: 90 | # { "window": "Window", "screen": "Screen.001", "workspace": "MyWorkspace" } 91 | # """ 92 | # if not kwargs_str.strip(): 93 | # return {} 94 | # kwargs = {} 95 | # for kv in kwargs_str.split(","): 96 | # kv = kv.strip() 97 | # if "=" not in kv: 98 | # continue 99 | # k, v = kv.split("=", 1) 100 | # kwargs[k.strip()] = v.strip() 101 | # return kwargs 102 | 103 | 104 | def find_area( 105 | area_or_type: Union[str, bpy.types.Area, None], 106 | screen_or_name: Union[str, bpy.types.Screen, None] = None 107 | ) -> Optional[bpy.types.Area]: 108 | """Find and return an Area object, or None if not found.""" 109 | try: 110 | if area_or_type is None: 111 | # return bpy.context.area # fallback 112 | return None 113 | 114 | if isinstance(area_or_type, bpy.types.Area): 115 | return area_or_type 116 | 117 | # Find screen 118 | screen = None 119 | if isinstance(screen_or_name, bpy.types.Screen): 120 | screen = screen_or_name 121 | elif isinstance(screen_or_name, str): 122 | screen = bpy.data.screens.get(screen_or_name) 123 | else: 124 | screen = bpy.context.screen 125 | 126 | if screen: 127 | for a in screen.areas: 128 | if a.type == area_or_type: 129 | return a 130 | 131 | except ReferenceError: 132 | # print_exc("find_area: invalid reference") 133 | pass 134 | 135 | return None 136 | 137 | 138 | def find_region( 139 | region_or_type: Union[str, bpy.types.Region, None], 140 | area_or_type: Union[str, bpy.types.Area, None] = None, 141 | screen_or_name: Union[str, bpy.types.Screen, None] = None 142 | ) -> Optional[bpy.types.Region]: 143 | """Find and return a Region object within the specified Area, or None if not found.""" 144 | try: 145 | if region_or_type is None: 146 | # return bpy.context.region # fallback 147 | return None 148 | 149 | if isinstance(region_or_type, bpy.types.Region): 150 | return region_or_type 151 | 152 | area = find_area(area_or_type, screen_or_name) 153 | if not area: 154 | return None 155 | 156 | for r in area.regions: 157 | if r.type == region_or_type: 158 | return r 159 | 160 | except ReferenceError: 161 | # print_exc("find_region: invalid reference") 162 | pass 163 | 164 | return None 165 | 166 | 167 | def find_window( 168 | value: Optional[Union[str, bpy.types.Window]], 169 | context: bpy.types.Context, 170 | ) -> Optional[bpy.types.Window]: 171 | """Resolve string or Window object into a Window object, fallback to context.window if none.""" 172 | if isinstance(value, bpy.types.Window): 173 | logi(f"find_window: {value}") 174 | return value 175 | # if isinstance(value, str): 176 | # if w := context.window_manager.windows.get(value, None): 177 | # return w 178 | # return None 179 | # logi(f"window fallback: {context.window}") 180 | # return context.window # fallback 181 | return None 182 | 183 | 184 | def find_screen( 185 | value: Optional[Union[str, bpy.types.Screen]], 186 | context: bpy.types.Context 187 | ) -> Optional[bpy.types.Screen]: 188 | """Resolve string or Screen object into a Screen object, fallback to context.screen if none.""" 189 | if isinstance(value, bpy.types.Screen): 190 | logi(f"find_screen: {value}") 191 | return value 192 | # if isinstance(value, str): 193 | # return bpy.data.screens.get(value) 194 | # logi(f"screen fallback: {context.screen}") 195 | # return context.screen # fallback 196 | return None 197 | 198 | 199 | class ContextOverride: 200 | def __init__( 201 | self, 202 | *, 203 | window: Union[str, bpy.types.Window, None] = None, 204 | screen: Union[str, bpy.types.Screen, None] = None, 205 | area: Union[str, bpy.types.Area, None] = None, 206 | region: Union[str, bpy.types.Region, None] = None, 207 | **kwargs: Any, 208 | ): 209 | self.window = window 210 | self.screen = screen 211 | self.area = area 212 | self.region = region 213 | self.kwargs = kwargs 214 | 215 | def validate( 216 | self, 217 | context: bpy.types.Context, 218 | *, 219 | # extra_priority: bool = False, 220 | delete_none: bool = True 221 | ) -> Dict[str, Any]: 222 | 223 | # Resolve all fields 224 | w = find_window(self.window, context) 225 | sc = find_screen(self.screen, context) 226 | a = find_area(self.area, sc) 227 | r = find_region(self.region, self.area, sc) 228 | # bd = self.blend_data # or context.blend_data 229 | 230 | base_dict = { 231 | "window": w, 232 | "screen": sc, 233 | "area": a, 234 | "region": r, 235 | # "blend_data": bd, 236 | } 237 | 238 | context_params = {**base_dict, **self.kwargs} 239 | 240 | if delete_none: 241 | context_params = {k: v for k, v in context_params.items() if v is not None} 242 | 243 | return context_params 244 | 245 | def __str__(self): 246 | return ( 247 | f"ContextOverride(\n" 248 | f" window={self.window},\n" 249 | f" screen={self.screen},\n" 250 | f" area={self.area},\n" 251 | f" region={self.region},\n" 252 | f" kwargs={self.kwargs})\n" 253 | ) 254 | 255 | 256 | def get_override_args( 257 | area: Union[str, bpy.types.Area] = None, 258 | region: Union[str, bpy.types.Region] = "WINDOW", 259 | screen: Union[str, bpy.types.Screen] = None, 260 | window: Union[str, bpy.types.Window] = None, 261 | delete_none: bool = True, 262 | **kwargs, 263 | ) -> dict: 264 | """Get a dictionary of context override arguments.""" 265 | override = ContextOverride( 266 | area=area, 267 | region=region, 268 | screen=screen, 269 | window=window, 270 | **kwargs, 271 | ) 272 | return override.validate(bpy.context, delete_none=delete_none) 273 | 274 | 275 | def focus_area(area, center=False, cmd=None): 276 | area = find_area(area) 277 | if not area: 278 | return 279 | 280 | event = pme.context.event 281 | move_flag = False 282 | if not event: 283 | center = True 284 | 285 | if center: 286 | x = area.x + area.width // 2 287 | y = area.y + area.height // 2 288 | move_flag = True 289 | else: 290 | x, y = event.mouse_x, event.mouse_y 291 | x = max(x, area.x) 292 | x = min(x, area.x + area.width - 1) 293 | y = max(y, area.y) 294 | y = min(y, area.y + area.height - 1) 295 | if x != event.mouse_x or y != event.mouse_y: 296 | move_flag = True 297 | 298 | if move_flag: 299 | bpy.context.window.cursor_warp(x, y) 300 | 301 | if cmd: 302 | with bpy.context.temp_override(area=area): 303 | bpy.ops.pme.timeout(cmd=cmd) 304 | 305 | 306 | # TODO: Remove this function 307 | def override_context( 308 | area, screen=None, window=None, region='WINDOW', **kwargs): 309 | # This is no longer necessary 310 | # but is documented in the old user docs so keeping it for now 311 | 312 | import traceback 313 | import warnings 314 | caller = traceback.extract_stack(None, 2)[0] 315 | warnings.warn( 316 | f"Deprecated: 'override_context' is deprecated, use 'get_override_args' instead. " 317 | f"Called from {caller.name} at {caller.line}", 318 | DeprecationWarning, 319 | stacklevel=2 320 | ) 321 | return get_override_args(area, region, screen, window, **kwargs) 322 | 323 | 324 | def toggle_sidebar(area=None, tools=True, value=None): 325 | area = find_area(area) 326 | 327 | s = area.spaces.active 328 | if tools and hasattr(s, "show_region_toolbar"): 329 | if value is None: 330 | value = not s.show_region_toolbar 331 | 332 | s.show_region_toolbar = value 333 | 334 | elif not tools and hasattr(s, "show_region_ui"): 335 | if value is None: 336 | value = not s.show_region_ui 337 | 338 | s.show_region_ui = value 339 | 340 | return True 341 | 342 | 343 | def register(): 344 | pme.context.add_global("focus_area", focus_area) 345 | pme.context.add_global("move_header", move_header) 346 | pme.context.add_global("toggle_sidebar", toggle_sidebar) 347 | pme.context.add_global("override_context", override_context) 348 | pme.context.add_global("redraw_screen", redraw_screen) 349 | -------------------------------------------------------------------------------- /scripts/autorun/functions.py: -------------------------------------------------------------------------------- 1 | 2 | # Do NOT edit this file. All changes will be lost 3 | 4 | 5 | def ups(): 6 | return bpy.context.tool_settings.unified_paint_settings 7 | 8 | 9 | def brush(use_ups=False): 10 | """ 11 | Return brush settings. 12 | Property Tab Usage: 13 | brush(ups().use_unified_size).size 14 | """ 15 | return ups() if use_ups else getattr(paint_settings(), "brush", None) 16 | 17 | 18 | def setattr_(object, name, value): 19 | setattr(object, name, value) 20 | return True 21 | 22 | 23 | def try_setattr(object, name, value): 24 | try: 25 | setattr(object, name, value) 26 | except: 27 | pass 28 | 29 | return True 30 | 31 | 32 | def event_mods(event): 33 | """ 34 | Return key modifiers (e.g. Ctrl+Shift, Alt+OSKey, None). 35 | Command Tab Usage: 36 | Call Stack Key's slot depending on key modifiers: 37 | open_menu("Stack Key", event_mods(E)) 38 | """ 39 | mods = "+".join( 40 | m for m in ["Ctrl", "Shift", "Alt", "OSKey"] 41 | if getattr(event, m.lower(), False)) 42 | 43 | return mods or "None" 44 | 45 | 46 | def raise_error(message): 47 | bpy.ops.pme.message_box(message=message, icon='ERROR') 48 | raise Exception(message) 49 | 50 | 51 | pme.context.add_global("ups", ups) 52 | pme.context.add_global("brush", brush) 53 | pme.context.add_global("setattr", setattr_) 54 | pme.context.add_global("try_setattr", try_setattr) 55 | pme.context.add_global("event_mods", event_mods) 56 | pme.context.add_global("raise_error", raise_error) 57 | -------------------------------------------------------------------------------- /scripts/command_area_join.py: -------------------------------------------------------------------------------- 1 | # Join 2 areas 2 | 3 | # Usage (Command tab): 4 | # execute_script("scripts/command_area_join.py") 5 | 6 | import bpy 7 | from pie_menu_editor import pme 8 | 9 | 10 | def join_area(): 11 | a = bpy.context.area 12 | if not a: 13 | return 14 | 15 | x, y, w, h = a.x, a.y, a.width, a.height 16 | if "cursor" in bpy.ops.screen.area_join.get_rna_type().properties: 17 | cursor = None 18 | for area in bpy.context.screen.areas: 19 | if a.x == area.x and a.width == area.width: 20 | if a.y + a.height + 1 == area.y: 21 | cursor = (x + 2, y + h - 2) 22 | break 23 | elif area.y + area.height + 1 == a.y: 24 | cursor = (x + 2, y) 25 | break 26 | if a.y == area.y and a.height == area.height: 27 | if a.x + a.width + 1 == area.x: 28 | cursor = (x + w - 2, y + 2) 29 | break 30 | elif area.x + area.width + 1 == a.x: 31 | cursor = (x, y + 2) 32 | break 33 | 34 | if cursor: 35 | bpy.ops.screen.area_join('INVOKE_DEFAULT', cursor=cursor) 36 | 37 | return 38 | 39 | r = (x + w + 2, y + h - 2, x + w - 2, y + h - 2) 40 | l = (x - 2, y + 2, x + 2, y + 2) 41 | t = (x + w - 2, y + h + 2, x + w - 2, y + h - 2) 42 | b = (x + 2, y - 2, x + 2, y + 2) 43 | 44 | mx, my = pme.context.event.mouse_x, pme.context.event.mouse_y 45 | cx, cy = x + 0.5 * w, y + 0.5 * h 46 | horizontal = (l, r) if mx < cx else (r, l) 47 | vertical = (b, t) if my < cy else (t, b) 48 | 49 | dx = min(mx - x, x + w - mx) 50 | dy = min(my - y, y + h - my) 51 | rects = vertical + horizontal if dy < dx else horizontal + vertical 52 | 53 | for rect in rects: 54 | if 'RUNNING_MODAL' in bpy.ops.screen.area_join( 55 | 'INVOKE_DEFAULT', min_x=rect[0], min_y=rect[1], 56 | max_x=rect[2], max_y=rect[3]): 57 | break 58 | 59 | 60 | join_area() 61 | -------------------------------------------------------------------------------- /scripts/command_area_move.py: -------------------------------------------------------------------------------- 1 | # Moves the edge (TOP, BOTTOM, LEFT or RIGHT) of the area 2 | 3 | # Usage (Command tab): 4 | # execute_script("scripts/command_area_move.py", area=C.area, edge='TOP', delta=300, move_cursor=False) 5 | 6 | import bpy 7 | 8 | 9 | def move_area(area, edge='TOP', delta=300, move_cursor=False): 10 | a = area or bpy.context.area 11 | if not a: 12 | return 13 | 14 | if edge not in {'BOTTOM', 'LEFT', 'RIGHT'}: 15 | edge = 'TOP' 16 | 17 | mx, my = E.mouse_x, E.mouse_y 18 | x, y = mx, my 19 | if edge == 'TOP': 20 | y = a.y + a.height 21 | my += delta * move_cursor 22 | elif edge == 'BOTTOM': 23 | y = a.y 24 | my += delta * move_cursor 25 | elif edge == 'RIGHT': 26 | x = a.x + a.width 27 | mx += delta * move_cursor 28 | elif edge == 'LEFT': 29 | x = a.x 30 | mx += delta * move_cursor 31 | 32 | bpy.context.window.cursor_warp(x, y) 33 | 34 | bpy.ops.pme.timeout( 35 | delay=0.0001, 36 | cmd=( 37 | "bpy.ops.screen.area_move(x=%d, y=%d, delta=%d);" 38 | "bpy.context.window.cursor_warp(%d, %d)" 39 | ) % (x, y, delta, mx, my) 40 | ) 41 | 42 | 43 | kwargs = locals().get("kwargs", {}) 44 | area = kwargs.get("area", C.area) 45 | edge = kwargs.get("edge", 'TOP') 46 | delta = kwargs.get("delta", 300) 47 | move_cursor = kwargs.get("move_cursor", False) 48 | 49 | move_area(area, edge, delta, move_cursor) 50 | -------------------------------------------------------------------------------- /scripts/command_area_split.py: -------------------------------------------------------------------------------- 1 | # Split the area 2 | # Press MMB to toggle direction 3 | 4 | # Usage (Command tab): 5 | # execute_script("scripts/command_area_split.py", mode='AUTO') 6 | 7 | import bpy 8 | 9 | 10 | def split_area(mode='AUTO'): 11 | a = bpy.context.area 12 | if not a: 13 | return 14 | 15 | if mode not in {'AUTO', 'VERTICAL', 'HORIZONTAL'}: 16 | mode = 'AUTO' 17 | 18 | if mode == 'AUTO': 19 | mode = 'VERTICAL' if a.width > a.height else 'HORIZONTAL' 20 | 21 | mx, my = None, None 22 | mx = a.x + a.width // 2 23 | if a.y != 0: 24 | my = a.y 25 | elif a.y + a.height != bpy.context.window.height: 26 | my = a.y + a.height 27 | vertical = None if mx is None or my is None else (mx, my) 28 | 29 | mx, my = None, None 30 | my = a.y + a.height // 2 31 | if a.x != 0: 32 | mx = a.x 33 | elif a.x + a.width != bpy.context.window.width: 34 | mx = a.x + a.width 35 | horizontal = None if mx is None or my is None else (mx, my) 36 | 37 | mouse = mode == 'VERTICAL' and vertical or horizontal or vertical 38 | if mouse: 39 | bpy.ops.screen.area_split( 40 | 'INVOKE_DEFAULT', cursor=[mouse[0], mouse[1]]) 41 | 42 | 43 | mode = locals().get("kwargs", {}).get("mode", 'AUTO') 44 | split_area(mode) 45 | -------------------------------------------------------------------------------- /scripts/command_context_sensitive_menu.py: -------------------------------------------------------------------------------- 1 | # Open context sensitive menu 2 | 3 | # Usage 1: 4 | # Import ./examples/context_sensitive_menu.json file 5 | 6 | # Usage 2 (Command tab): 7 | # execute_script("scripts/command_context_sensitive_menu.py") 8 | 9 | # Usage 3 (Command tab): 10 | # from .scripts.command_context_sensitive_menu import open_csm; open_csm() 11 | 12 | import bpy 13 | from pie_menu_editor import pme 14 | 15 | 16 | def open_csm(prefix="", suffix=""): 17 | p, s = prefix, suffix 18 | context = bpy.context 19 | obj = context.selected_objects and context.active_object 20 | open_menu = pme.context.open_menu 21 | 22 | if not obj: 23 | open_menu(p + "None Object" + s) 24 | 25 | elif obj.type == "MESH": 26 | if obj.mode == 'EDIT': 27 | msm = context.tool_settings.mesh_select_mode 28 | msm[0] and open_menu(p + "Vertex" + s) or \ 29 | msm[1] and open_menu(p + "Edge" + s) or \ 30 | msm[2] and open_menu(p + "Face" + s) or \ 31 | open_menu(p + "Edit" + s) or \ 32 | open_menu(p + "Mesh" + s) or \ 33 | open_menu(p + "Any Object" + s) 34 | 35 | else: 36 | open_menu(p + obj.mode.replace("_", " ").title() + s) or \ 37 | open_menu(p + "Mesh" + s) or \ 38 | open_menu(p + "Any Object" + s) 39 | 40 | else: 41 | open_menu(p + obj.mode.replace("_", " ").title() + s) or \ 42 | open_menu(p + obj.type.replace("_", " ").title() + s) or \ 43 | open_menu(p + "Any Object" + s) 44 | 45 | 46 | kwargs = locals().get("kwargs", {}) 47 | prefix = kwargs.get("prefix", "") 48 | suffix = kwargs.get("suffix", "") 49 | open_csm(prefix, suffix) 50 | -------------------------------------------------------------------------------- /scripts/command_hello_world.py: -------------------------------------------------------------------------------- 1 | # Open a pop-up message 2 | 3 | # Usage 1 (Command tab): 4 | # execute_script("scripts/command_menu.py", msg="My Message") 5 | 6 | # Usage 2 (Command tab): 7 | # from .scripts.command_menu import say_hello; say_hello("My Message") 8 | 9 | import bpy 10 | 11 | 12 | def say_hello(msg): 13 | def draw(menu, context): 14 | menu.layout.label(text=msg, icon='BLENDER') 15 | 16 | bpy.context.window_manager.popup_menu(draw, title="My Popup") 17 | 18 | 19 | msg = locals().get("kwargs", {}).get("msg", "Hello World!") 20 | say_hello(msg) 21 | -------------------------------------------------------------------------------- /scripts/command_localview.py: -------------------------------------------------------------------------------- 1 | # Toggle Local/Global view mode without changing the view 2 | 3 | # Usage 1 (Command tab): 4 | # execute_script("scripts/command_localview.py", mode='TOGGLE') 5 | 6 | # Usage 2 (Command tab): 7 | # from .scripts.command_localview import localview; localview('TOGGLE') 8 | 9 | import bpy 10 | 11 | 12 | def localview(mode='TOGGLE'): 13 | context = bpy.context 14 | area = context.area 15 | area_type = area.ui_type if hasattr(area, "ui_type") else area.type 16 | if area_type != 'VIEW_3D': 17 | return 18 | 19 | if mode not in {'TOGGLE', 'LOCAL', 'GLOBAL'}: 20 | mode = 'TOGGLE' 21 | 22 | if len(context.space_data.region_quadviews): 23 | regions = context.space_data.region_quadviews 24 | else: 25 | regions = [context.space_data.region_3d] 26 | 27 | view_data = [ 28 | dict( 29 | view_camera_offset=region.view_camera_offset, 30 | view_camera_zoom=region.view_camera_zoom, 31 | view_distance=region.view_distance, 32 | view_location=region.view_location.copy(), 33 | view_matrix=region.view_matrix.copy(), 34 | view_perspective=region.view_perspective, 35 | is_perspective=region.is_perspective, 36 | view_rotation=region.view_rotation.copy(), 37 | ) for region in regions] 38 | 39 | upr = getattr(context, "user_preferences", context.preferences) 40 | smooth_view = upr.view.smooth_view 41 | upr.view.smooth_view = 0 42 | 43 | if mode == 'TOGGLE' or \ 44 | mode == 'LOCAL' and not context.space_data.local_view or \ 45 | mode == 'GLOBAL' and context.space_data.local_view: 46 | bpy.ops.view3d.localview() 47 | 48 | upr.view.smooth_view = smooth_view 49 | for region, data in zip(regions, view_data): 50 | for k, v in data.items(): 51 | setattr(region, k, v) 52 | 53 | 54 | mode = locals().get("kwargs", {}).get("mode", 'TOGGLE') 55 | localview(mode) 56 | -------------------------------------------------------------------------------- /scripts/command_panel.py: -------------------------------------------------------------------------------- 1 | # Open side panel with a single tab/category 2 | 3 | # Usage (Command tab): 4 | 5 | # Open panels by tab/category 6 | # from .scripts.command_panel import open_tab; open_tab("Tools", region='ANY') 7 | 8 | # Open panel by name 9 | # from .scripts.command_panel import open_panel; open_panel("3D Cursor", region='ANY') 10 | 11 | # Restore hidden panels 12 | # from .scripts.command_panel import restore_panels; restore_panels() 13 | 14 | import bpy 15 | from inspect import isclass 16 | 17 | 18 | hidden_panels = {} 19 | 20 | 21 | @classmethod 22 | def _dummy_poll(cls, context): 23 | return False 24 | 25 | 26 | def _panel_types(): 27 | ret = [] 28 | panel_tp = bpy.types.Panel 29 | for tp_name in dir(bpy.types): 30 | tp = getattr(bpy.types, tp_name) 31 | if tp == panel_tp or not isclass(tp) or not issubclass(tp, panel_tp): 32 | continue 33 | 34 | ret.append(tp) 35 | 36 | return ret 37 | 38 | 39 | def _reregister_panel(tp): 40 | try: 41 | bpy.utils.unregister_class(tp) 42 | bpy.utils.register_class(tp) 43 | except: 44 | pass 45 | 46 | 47 | def _hide_panel(tp, context): 48 | area = context.area.type 49 | if area not in hidden_panels: 50 | hidden_panels[area] = {} 51 | 52 | panels = hidden_panels.get(context.area.type, None) 53 | if tp.__name__ in panels: 54 | return 55 | panels[tp.__name__] = hasattr(tp, "poll") and tp.poll 56 | tp.poll = _dummy_poll 57 | 58 | _reregister_panel(tp) 59 | 60 | 61 | def _restore_panel(tp, context): 62 | panels = hidden_panels.get(context.area.type, None) 63 | if panels and tp.__name__ in panels: 64 | poll = panels.pop(tp.__name__) 65 | if poll: 66 | tp.poll = poll 67 | else: 68 | delattr(tp, "poll") 69 | 70 | _reregister_panel(tp) 71 | 72 | 73 | def restore_panels(): 74 | context = bpy.context 75 | panels = hidden_panels.get(context.area.type, None) 76 | if not panels: 77 | return 78 | 79 | for tp in _panel_types(): 80 | _restore_panel(tp, context) 81 | 82 | context.area.tag_redraw() 83 | 84 | 85 | def _open_panels_by(name=None, category=None, region='ANY'): 86 | context = bpy.context 87 | side_regions = {'TOOLS', 'UI'} 88 | if region not in side_regions: 89 | region = 'ANY' 90 | 91 | panels_to_show = [] 92 | panels_to_hide = [] 93 | for tp in _panel_types(): 94 | if tp.bl_space_type != context.area.type or \ 95 | tp.bl_region_type not in side_regions: 96 | continue 97 | if name and getattr(tp, "bl_label", None) == name: 98 | panels_to_show.append(tp) 99 | elif category and getattr(tp, "bl_category", "Misc") == category: 100 | panels_to_show.append(tp) 101 | else: 102 | panels_to_hide.append(tp) 103 | 104 | if not panels_to_show: 105 | return 106 | 107 | for tp in panels_to_show: 108 | if region == 'ANY': 109 | region = tp.bl_region_type 110 | _restore_panel(tp, context) 111 | 112 | for tp in panels_to_hide: 113 | if tp.bl_region_type == region: 114 | _hide_panel(tp, context) 115 | 116 | context.area.tag_redraw() 117 | 118 | for r in context.area.regions: 119 | if r.type == region and r.width <= 1: 120 | bpy.ops.wm.pme_sidebar_toggle(tools=region == 'TOOLS') 121 | 122 | 123 | def open_tab(category, region='ANY'): 124 | _open_panels_by(category=category, region=region) 125 | 126 | 127 | def open_panel(name, region='ANY'): 128 | _open_panels_by(name=name, region=region) 129 | -------------------------------------------------------------------------------- /scripts/command_return_value.py: -------------------------------------------------------------------------------- 1 | # Return a value 2 | 3 | # Usage 1: 4 | # print(execute_script("scripts/command_return.py")) 5 | 6 | return_value = "My Value" 7 | 8 | # Usage 2: 9 | # from .scripts.command_return import get_value; print(get_value()) 10 | 11 | 12 | def get_value(): 13 | return "My Value" 14 | -------------------------------------------------------------------------------- /scripts/custom_hello_world.py: -------------------------------------------------------------------------------- 1 | # Draw label 2 | 3 | # Usage (Custom tab): 4 | # execute_script("scripts/custom_hello_world.py", msg="My Message") 5 | 6 | msg = kwargs.get("msg", pme.context.text or "Hello World!") 7 | 8 | box = L.box() 9 | box.label( 10 | text=msg, 11 | icon=pme.context.icon, 12 | icon_value=pme.context.icon_value) 13 | -------------------------------------------------------------------------------- /selection_state.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def _get_area(): 5 | return bpy.context.area and bpy.context.area.type 6 | 7 | 8 | def _get_mode(atype): 9 | if not atype: 10 | return None 11 | 12 | if atype == 'PROPERTIES': 13 | return bpy.context.space_data.context 14 | 15 | if atype in {'VIEW_3D', 'IMAGE_EDITOR'}: 16 | return bpy.context.mode 17 | 18 | return None 19 | 20 | 21 | def _get_submode(atype): 22 | if not atype: 23 | return None 24 | 25 | C = bpy.context 26 | ao = C.active_object 27 | if not ao: 28 | return None 29 | 30 | if atype == 'VIEW_3D': 31 | if ao.mode == 'EDIT': 32 | if ao.type == 'MESH': 33 | sm = C.tool_settings.mesh_select_mode 34 | return "%d,%d,%d" % (sm[0], sm[1], sm[2]) 35 | 36 | elif atype == 'IMAGE_EDITOR': 37 | if C.space_data.mode != 'PAINT': 38 | return C.tool_settings.uv_select_mode 39 | 40 | return None 41 | 42 | 43 | def _get_ao(): 44 | if bpy.context.active_operator: 45 | return bpy.context.active_operator.bl_idname 46 | return None 47 | 48 | 49 | class BlenderState: 50 | def __init__(self): 51 | self.area = None 52 | self.mode = None 53 | self.submode = None 54 | 55 | def update(self): 56 | self.area = _get_area() 57 | self.mode = _get_mode(self.area) 58 | self.submode = _get_submode(self.area) 59 | 60 | def __str__(self): 61 | return "[%s] %s" % (self.mode, self.submode) 62 | 63 | 64 | _state = BlenderState() 65 | 66 | 67 | def check(): 68 | area = _get_area() 69 | if _state.area != area: 70 | return False 71 | 72 | mode = _get_mode(area) 73 | if _state.mode != mode: 74 | return False 75 | 76 | submode = _get_submode(area) 77 | if _state.submode != submode: 78 | return False 79 | 80 | return True 81 | 82 | 83 | def update(): 84 | _state.update() 85 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import traceback 3 | from . import constants as CC 4 | from .debug_utils import * 5 | from . import operator_utils 6 | from . import pme 7 | from .bl_utils import bp, ctx_dict 8 | from .addon import uprefs, ic 9 | 10 | uilayout_getattribute = bpy.types.UILayout.__getattribute__ 11 | draw_addons_default = None 12 | 13 | 14 | def utitle(text): 15 | words = [word[0].upper() + word[1:] for word in text.split("_") if word] 16 | return " ".join(words) 17 | 18 | 19 | def shorten_str(value, n=50): 20 | return value[:n] + (value[n:] and "...") 21 | 22 | 23 | def find_enum_args(mo): 24 | args = mo.group(2)[1:-1] 25 | args = args.split(",") 26 | args = [arg.split("=")[0].strip() for arg in args] 27 | if not args: 28 | return False 29 | 30 | enum_args = [] 31 | 32 | try: 33 | op_rna_type = operator_utils.get_rna_type(mo.group(1)) 34 | if op_rna_type: 35 | properties = op_rna_type.bl_rna.properties 36 | for arg in args: 37 | if arg in properties: 38 | if properties[arg].type == 'ENUM': 39 | enum_args.append(arg) 40 | elif hasattr(tp, arg): 41 | if 'Enum' in getattr(tp, arg)[0]: 42 | enum_args.append(arg) 43 | 44 | except: 45 | pass 46 | 47 | return enum_args 48 | 49 | 50 | def gen_op_name(mo, strict=False): 51 | name = operator_utils.operator_label(mo.group(1)) 52 | if not name: 53 | name = "" if strict else shorten_str(mo.group(1) + mo.group(2)) 54 | 55 | return name 56 | 57 | 58 | def gen_prop_name(mo, is_prop=False, strict=False): 59 | name = mo.group(2) 60 | 61 | if not is_prop: 62 | name += mo.group(3) 63 | name = "" if strict else shorten_str(name) 64 | icon = "" 65 | 66 | prop_path = mo.group(1) + mo.group(2) 67 | 68 | if is_prop and prop_path[-1] == "]": 69 | prop_path, _, _ = prop_path.rpartition("[") 70 | 71 | prop = bp.get(prop_path) 72 | 73 | if prop: 74 | name = prop.name 75 | icon = prop.icon 76 | if not is_prop: 77 | name += mo.group(3) 78 | 79 | return name, icon 80 | 81 | 82 | def tag_redraw(all=False): 83 | if all: 84 | tag_redraw_windows() 85 | else: 86 | tag_redraw_windows(CC.UPREFS, 'WINDOW') 87 | 88 | 89 | def tag_redraw_windows(area=None, region=None): 90 | wm = bpy.context.window_manager 91 | if not wm: 92 | return True 93 | 94 | for w in wm.windows: 95 | for a in w.screen.areas: 96 | if area is None or a.type == area or \ 97 | area == CC.UPREFS and not a.type: 98 | for r in a.regions: 99 | if region is None or r.type == region: 100 | r.tag_redraw() 101 | 102 | return True 103 | 104 | 105 | def draw_addons_maximized(self, context): 106 | layout = self.layout 107 | 108 | if PME_OT_userpref_show.mod != "pie_menu_editor": 109 | row = self.layout.row(align=True) 110 | row.scale_y = 1.5 111 | row.operator(PME_OT_userpref_restore.bl_idname, text="Restore") 112 | 113 | prefs = uprefs().addons[PME_OT_userpref_show.mod].preferences 114 | 115 | draw = getattr(prefs, "draw", None) 116 | prefs_class = type(prefs) 117 | layout = layout.box() 118 | prefs_class.layout = layout 119 | try: 120 | draw(context) 121 | except: 122 | DBG and loge(traceback.format_exc()) 123 | layout.label(text="Error (see console)", icon=ic('ERROR')) 124 | del prefs_class.layout 125 | 126 | 127 | class PME_OT_userpref_show(bpy.types.Operator): 128 | bl_idname = "pme.userpref_show" 129 | bl_label = "User Preferences" 130 | bl_options = {'INTERNAL'} 131 | 132 | mod = None 133 | 134 | tab: bpy.props.StringProperty(options={'SKIP_SAVE'}) 135 | addon: bpy.props.StringProperty(options={'SKIP_SAVE'}) 136 | 137 | def execute(self, context): 138 | if context.area.type != CC.UPREFS: 139 | bpy.ops.screen.userpref_show('INVOKE_DEFAULT') 140 | 141 | if self.addon: 142 | PME_OT_userpref_show.mod = self.addon 143 | bpy.types.USERPREF_PT_addons.draw = draw_addons_maximized 144 | self.tab = 'ADDONS' 145 | 146 | else: 147 | bpy.types.USERPREF_PT_addons.draw = draw_addons_default 148 | 149 | if self.tab: 150 | uprefs().active_section = self.tab 151 | 152 | tag_redraw() 153 | return {'FINISHED'} 154 | 155 | 156 | class PME_OT_userpref_restore(bpy.types.Operator): 157 | bl_idname = "pme.userpref_restore" 158 | bl_label = "Restore User Preferences Area" 159 | 160 | def execute(self, context): 161 | bpy.types.USERPREF_PT_addons.draw = draw_addons_default 162 | return {'FINISHED'} 163 | 164 | 165 | def pme_uilayout_getattribute(self, attr): 166 | def pme_operator( 167 | operator, text="", 168 | text_ctxt="", translate=True, icon='NONE', 169 | emboss=True, icon_value=0): 170 | uilayout_operator = uilayout_getattribute(self, "operator") 171 | 172 | return uilayout_operator( 173 | operator, text=text, 174 | text_ctxt=text_ctxt, translate=translate, 175 | icon=icon, emboss=emboss, icon_value=icon_value) 176 | 177 | if attr == "operator": 178 | return pme_operator 179 | 180 | return uilayout_getattribute(self, attr) 181 | 182 | 183 | def is_userpref_maximized(): 184 | return bpy.types.USERPREF_PT_addons.draw == draw_addons_maximized 185 | 186 | 187 | def register(): 188 | # bpy.types.UILayout.__getattribute__ = pme_uilayout_getattribute 189 | global draw_addons_default 190 | draw_addons_default = bpy.types.USERPREF_PT_addons.draw 191 | 192 | pme.context.add_global("tag_redraw", tag_redraw_windows) 193 | 194 | 195 | def unregister(): 196 | # bpy.types.UILayout.__getattribute__ = uilayout_getattribute 197 | if bpy.types.USERPREF_PT_addons.draw == draw_addons_maximized: 198 | bpy.types.USERPREF_PT_addons.draw = draw_addons_default 199 | -------------------------------------------------------------------------------- /ui_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import sys 4 | import marshal 5 | import py_compile 6 | from traceback import format_exc 7 | from errno import ENOENT 8 | from .addon import ADDON_PATH, prefs, print_exc 9 | from . import pme 10 | from .layout_helper import lh, draw_pme_layout, CLayout 11 | from .operators import WM_OT_pme_user_pie_menu_call 12 | 13 | 14 | class WM_MT_pme: 15 | bl_label = "" 16 | 17 | def draw(self, context): 18 | pr = prefs() 19 | pm = pr.pie_menus[self.bl_label] 20 | 21 | row = self.layout.row() 22 | lh.column(row, operator_context='INVOKE_DEFAULT') 23 | 24 | for idx, pmi in enumerate(pm.pmis): 25 | if pmi.mode == 'EMPTY': 26 | if pmi.text == "": 27 | lh.sep() 28 | elif pmi.text == "spacer": 29 | lh.label(" ") 30 | elif pmi.text == "column": 31 | lh.column(row, operator_context='INVOKE_DEFAULT') 32 | elif pmi.text == "label": 33 | text, icon, *_ = pmi.parse() 34 | lh.label(text, icon) 35 | continue 36 | 37 | WM_OT_pme_user_pie_menu_call._draw_item(pr, pm, pmi, idx) 38 | 39 | 40 | pme_menu_classes = {} 41 | 42 | 43 | def get_pme_menu_class(name): 44 | if name not in pme_menu_classes: 45 | class_name = "PME_MT_menu_%d" % len(pme_menu_classes) 46 | pme_menu_classes[name] = type( 47 | class_name, 48 | (WM_MT_pme, bpy.types.Menu), { 49 | 'bl_label': name, 50 | }) 51 | bpy.utils.register_class(pme_menu_classes[name]) 52 | 53 | return pme_menu_classes[name].__name__ 54 | 55 | 56 | def accordion(layout, data, prop): 57 | ret = None 58 | enum_items = data.rna_type.properties[prop].enum_items 59 | value = getattr(data, prop) 60 | for item in enum_items: 61 | layout.prop_enum(data, prop, item.identifier) 62 | if item.identifier == value: 63 | ret = layout.box().column() 64 | return ret 65 | 66 | 67 | def header_menu(areas): 68 | ctx = bpy.context 69 | 70 | def draw_menus(area, menu_types, layout): 71 | if area == "CLIP": 72 | sd = None 73 | if ctx.space_data and ctx.space_data.type == 'CLIP_EDITOR': 74 | sd = ctx.space_data 75 | else: 76 | for screen in bpy.data.screens: 77 | for a in screen.areas: 78 | if a.type == 'CLIP_EDITOR': 79 | sd = a.spaces[0] 80 | break 81 | if sd: 82 | break 83 | 84 | if not sd: 85 | menu_type = "None" 86 | elif sd.mode == 'TRACKING': 87 | menu_type = "CLIP_MT_tracking_editor_menus" 88 | elif sd.mode == 'MASK': 89 | menu_type = "CLIP_MT_masking_editor_menus" 90 | 91 | elif area in menu_types: 92 | menu_type = menu_types[area] 93 | 94 | else: 95 | menu_type = area + "_MT_editor_menus" 96 | 97 | tp = getattr(bpy.types, menu_type, None) 98 | if tp: 99 | row = layout.row() 100 | row.alignment = 'CENTER' 101 | pm = pme.context.pm 102 | prop = pme.props.parse(pm.data) 103 | if pm.mode != 'DIALOG' or not prop.pd_panel and not prop.pd_box: 104 | row = row.box().row() 105 | 106 | def get_space_data_attribute(self, attr): 107 | if attr == "space_data": 108 | return sd 109 | del bpy.types.Context.__getattribute__ 110 | return getattr(self, attr) 111 | 112 | bpy.types.Context.__getattribute__ = get_space_data_attribute 113 | 114 | if hasattr(tp, "draw_collapsible"): 115 | row.emboss = 'PULLDOWN_MENU' 116 | tp.draw_collapsible(ctx, row) 117 | else: 118 | CLayout.use_mouse_over_open = True 119 | tp.draw_menus(row, ctx) 120 | CLayout.use_mouse_over_open = None 121 | 122 | if not isinstance(areas, list): 123 | areas = [areas] 124 | 125 | menu_types = dict( 126 | TIMELINE="TIME_MT_editor_menus", 127 | IMAGE="MASK_MT_editor_menus", 128 | SEQUENCE="SEQUENCER_MT_editor_menus", 129 | ) 130 | 131 | try: 132 | col = pme.context.layout.column() 133 | for a in areas: 134 | if not a or a == 'CURRENT': 135 | a = ctx.area.type 136 | a = a.replace("_EDITOR", "").replace("_", "") 137 | 138 | draw_menus(a, menu_types, col) 139 | except: 140 | print_exc() 141 | 142 | return True 143 | 144 | 145 | def execute_script(path, **kwargs): 146 | if not os.path.isabs(path): 147 | path = os.path.join(ADDON_PATH, path) 148 | path = os.path.normpath(path) 149 | 150 | if not os.path.isfile(path): 151 | raise OSError(ENOENT, os.strerror(ENOENT), path) 152 | 153 | exec_globals = pme.context.gen_globals() 154 | exec_globals["kwargs"] = kwargs 155 | exec_globals["__file__"] = path 156 | 157 | pr = prefs() 158 | if pr.cache_scripts: 159 | name = os.path.basename(path) 160 | name, _, _ = name.rpartition(".") 161 | cname = name + ".cpython-%d%d.pyc" % ( 162 | sys.version_info[0], sys.version_info[1]) 163 | cpath = os.path.join(os.path.dirname(path), "__pycache__", cname) 164 | 165 | try: 166 | if os.path.isfile(cpath): 167 | cmod_time = os.stat(cpath).st_mtime 168 | mod_time = os.stat(path).st_mtime 169 | if mod_time > cmod_time: 170 | cpath = py_compile.compile(path) 171 | else: 172 | cpath = py_compile.compile(path) 173 | 174 | with open(cpath, "rb") as f: 175 | if sys.version_info >= (3, 7, 0): 176 | f.read(16) 177 | else: 178 | f.read(12) 179 | 180 | exec(marshal.load(f), exec_globals) 181 | except: 182 | if pr.debug_mode: 183 | s = format_exc() 184 | print(s) 185 | if pme.context.exec_operator: 186 | pme.context.exec_operator.report({'ERROR'}, s) 187 | 188 | else: 189 | try: 190 | with open(path) as f: 191 | exec(f.read(), exec_globals) 192 | except: 193 | if pr.debug_mode: 194 | s = format_exc() 195 | print(s) 196 | if pme.context.exec_operator: 197 | pme.context.exec_operator.report({'ERROR'}, s) 198 | 199 | return exec_globals.get("return_value", True) 200 | 201 | 202 | def draw_menu(name, frame=False, dx=0, dy=0, layout=None): 203 | pr = prefs() 204 | if name in pr.pie_menus: 205 | lh.save() 206 | if layout: 207 | lh.lt(layout) 208 | 209 | orow, ocol, drow, dcol = None, None, None, None 210 | 211 | if dx != 0 or dy != 0: 212 | if dx != 0: 213 | orow = lh.row() 214 | if dx > 0: 215 | drow = orow.row(align=True) 216 | drow.separator() 217 | drow.scale_x = dx 218 | 219 | if dy != 0: 220 | ocol = lh.column() 221 | if dy < 0: 222 | dcol = ocol.column(align=True) 223 | dcol.separator() 224 | dcol.scale_y = -dy 225 | 226 | if frame: 227 | lh.box() 228 | 229 | lh.column() 230 | 231 | draw_pme_layout( 232 | pr.pie_menus[name], lh.layout, 233 | WM_OT_pme_user_pie_menu_call._draw_item) 234 | 235 | if dx < 0: 236 | drow = orow.row(align=True) 237 | drow.separator() 238 | drow.scale_x = -dx 239 | 240 | if dy > 0: 241 | dcol = ocol.column(align=True) 242 | dcol.separator() 243 | dcol.scale_y = dy 244 | 245 | lh.restore() 246 | return True 247 | return False 248 | 249 | 250 | def open_menu(name, slot=None, **kwargs): 251 | pr = prefs() 252 | if name in pr.pie_menus: 253 | invoke_mode = 'RELEASE' 254 | if pme.context.pm and pme.context.pm.mode == 'SCRIPT': 255 | invoke_mode = 'HOTKEY' 256 | 257 | pme.context.exec_user_locals.update(kwargs) 258 | 259 | if slot is None: 260 | slot = -1 261 | elif isinstance(slot, str): 262 | # pmi_name = slot 263 | slot = pr.pie_menus[name].pmis.find(slot) 264 | if slot == -1: 265 | return False 266 | # pm = pr.pie_menus[name] 267 | # for i, pmi in enumerate(pm.pmis): 268 | # if pmi.name == pmi_name: 269 | # slot = i 270 | # break 271 | 272 | bpy.ops.wm.pme_user_pie_menu_call( 273 | 'INVOKE_DEFAULT', pie_menu_name=name, 274 | # invoke_mode=pme.context.last_operator.invoke_mode) 275 | invoke_mode=invoke_mode, 276 | slot=slot) 277 | 278 | pme.context.exec_user_locals.clear() 279 | return True 280 | 281 | return False 282 | 283 | 284 | def toggle_menu(name, value=None): 285 | pr = prefs() 286 | if name in pr.pie_menus: 287 | pm = pr.pie_menus[name] 288 | if value is None: 289 | value = not pm.enabled 290 | pm.enabled = value 291 | return True 292 | return False 293 | 294 | 295 | def register(): 296 | pme.context.add_global("header_menu", header_menu) 297 | pme.context.add_global("draw_menu", draw_menu) 298 | pme.context.add_global("open_menu", open_menu) 299 | pme.context.add_global("execute_script", execute_script) 300 | pme.context.add_global("toggle_menu", toggle_menu) 301 | 302 | 303 | def unregister(): 304 | for cl in pme_menu_classes.values(): 305 | bpy.utils.unregister_class(cl) 306 | 307 | pme_menu_classes.clear() 308 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from threading import Thread, Event, Lock, current_thread 2 | from time import time, sleep 3 | from sys import exc_info 4 | from traceback import format_exception_only 5 | 6 | lock = Lock() 7 | lock2 = Lock() 8 | 9 | 10 | class Timer(Thread): 11 | def __init__(self, interval, function): 12 | Thread.__init__(self) 13 | self.interval = interval 14 | self.function = function 15 | self.stopped = Event() 16 | # self.daemon = True 17 | self.time = time() 18 | 19 | @property 20 | def elapsed_time(self): 21 | return time() - self.time 22 | 23 | def run(self): 24 | while not self.stopped.wait(self.interval): 25 | with lock: 26 | self.function() 27 | # with lock2: 28 | # pass 29 | 30 | def cancel(self): 31 | self.stopped.set() 32 | 33 | 34 | def multiton(cls): 35 | instances = {} 36 | 37 | def get_instance(id): 38 | if id not in instances: 39 | instances[id] = cls(id) 40 | return instances[id] 41 | 42 | return get_instance 43 | 44 | 45 | def extract_str_flags(text, *flags): 46 | ret_flags = [False] * len(flags) 47 | if not text: 48 | return ("", *ret_flags) 49 | 50 | for i, f in enumerate(flags): 51 | if text.startswith(f): 52 | ret_flags[i] = True 53 | text = text[len(f):] 54 | 55 | return (text, *ret_flags) 56 | 57 | 58 | def extract_str_flags_b(text, *flags): 59 | ret_flags = [False] * len(flags) 60 | if not text: 61 | return ("", *ret_flags) 62 | 63 | for i, f in reversed(list(enumerate(flags))): 64 | if text.endswith(f): 65 | ret_flags[i] = True 66 | text = text[:-len(f)] 67 | 68 | return (text, *ret_flags) 69 | 70 | 71 | def format_exception(idx=None): 72 | ei = exc_info() 73 | if idx is None: 74 | return "".join(format_exception_only(ei[0], ei[1])).rstrip("\n") 75 | else: 76 | ret = format_exception_only(ei[0], ei[1]) 77 | if not ret: 78 | return "" 79 | ret = ret[idx % len(ret)].rstrip("\n") 80 | return ret 81 | 82 | 83 | def isclose(a, b): 84 | mod = max(abs(a) % 1, abs(b) % 1) or 1 85 | return abs(a - b) <= 1e-05 * mod 86 | --------------------------------------------------------------------------------