86eea422cf9c09f544fb130674f3ce5e8b6e0e81
[blender.git] / release / scripts / modules / bpy / utils / __init__.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8-80 compliant>
20
21 """
22 This module contains utility functions specific to blender but
23 not associated with blenders internal data.
24 """
25
26 __all__ = (
27     "blend_paths",
28     "escape_identifier",
29     "keyconfig_init",
30     "keyconfig_set",
31     "load_scripts",
32     "modules_from_path",
33     "preset_find",
34     "preset_paths",
35     "refresh_script_paths",
36     "app_template_paths",
37     "register_class",
38     "register_manual_map",
39     "unregister_manual_map",
40     "register_classes_factory",
41     "register_submodule_factory",
42     "register_tool",
43     "make_rna_paths",
44     "manual_map",
45     "previews",
46     "resource_path",
47     "script_path_user",
48     "script_path_pref",
49     "script_paths",
50     "smpte_from_frame",
51     "smpte_from_seconds",
52     "units",
53     "unregister_class",
54     "unregister_tool",
55     "user_resource",
56     "execfile",
57 )
58
59 from _bpy import (
60     _utils_units as units,
61     blend_paths,
62     escape_identifier,
63     register_class,
64     resource_path,
65     script_paths as _bpy_script_paths,
66     unregister_class,
67     user_resource as _user_resource,
68 )
69
70 import bpy as _bpy
71 import os as _os
72 import sys as _sys
73
74 import addon_utils as _addon_utils
75
76 _preferences = _bpy.context.preferences
77 _script_module_dirs = "startup", "modules"
78 _is_factory_startup = _bpy.app.factory_startup
79
80
81 def execfile(filepath, mod=None):
82     # module name isn't used or added to 'sys.modules'.
83     # passing in 'mod' allows re-execution without having to reload.
84
85     import importlib.util
86     mod_spec = importlib.util.spec_from_file_location("__main__", filepath)
87     if mod is None:
88         mod = importlib.util.module_from_spec(mod_spec)
89     mod_spec.loader.exec_module(mod)
90     return mod
91
92
93 def _test_import(module_name, loaded_modules):
94     use_time = _bpy.app.debug_python
95
96     if module_name in loaded_modules:
97         return None
98     if "." in module_name:
99         print("Ignoring '%s', can't import files containing "
100               "multiple periods" % module_name)
101         return None
102
103     if use_time:
104         import time
105         t = time.time()
106
107     try:
108         mod = __import__(module_name)
109     except:
110         import traceback
111         traceback.print_exc()
112         return None
113
114     if use_time:
115         print("time %s %.4f" % (module_name, time.time() - t))
116
117     loaded_modules.add(mod.__name__)  # should match mod.__name__ too
118     return mod
119
120
121 def _sys_path_ensure(path):
122     if path not in _sys.path:  # reloading would add twice
123         _sys.path.insert(0, path)
124
125
126 def modules_from_path(path, loaded_modules):
127     """
128     Load all modules in a path and return them as a list.
129
130     :arg path: this path is scanned for scripts and packages.
131     :type path: string
132     :arg loaded_modules: already loaded module names, files matching these
133        names will be ignored.
134     :type loaded_modules: set
135     :return: all loaded modules.
136     :rtype: list
137     """
138     modules = []
139
140     for mod_name, _mod_path in _bpy.path.module_names(path):
141         mod = _test_import(mod_name, loaded_modules)
142         if mod:
143             modules.append(mod)
144
145     return modules
146
147
148 _global_loaded_modules = []  # store loaded module names for reloading.
149 import bpy_types as _bpy_types  # keep for comparisons, never ever reload this.
150
151
152 def load_scripts(reload_scripts=False, refresh_scripts=False):
153     """
154     Load scripts and run each modules register function.
155
156     :arg reload_scripts: Causes all scripts to have their unregister method
157        called before loading.
158     :type reload_scripts: bool
159     :arg refresh_scripts: only load scripts which are not already loaded
160        as modules.
161     :type refresh_scripts: bool
162     """
163     use_time = use_class_register_check = _bpy.app.debug_python
164     use_user = not _is_factory_startup
165
166     if use_time:
167         import time
168         t_main = time.time()
169
170     loaded_modules = set()
171
172     if refresh_scripts:
173         original_modules = _sys.modules.values()
174
175     if reload_scripts:
176         # just unload, don't change user defaults, this means we can sync
177         # to reload. note that they will only actually reload of the
178         # modification time changes. This `won't` work for packages so...
179         # its not perfect.
180         for module_name in [ext.module for ext in _preferences.addons]:
181             _addon_utils.disable(module_name)
182
183     def register_module_call(mod):
184         register = getattr(mod, "register", None)
185         if register:
186             try:
187                 register()
188             except:
189                 import traceback
190                 traceback.print_exc()
191         else:
192             print("\nWarning! '%s' has no register function, "
193                   "this is now a requirement for registerable scripts" %
194                   mod.__file__)
195
196     def unregister_module_call(mod):
197         unregister = getattr(mod, "unregister", None)
198         if unregister:
199             try:
200                 unregister()
201             except:
202                 import traceback
203                 traceback.print_exc()
204
205     def test_reload(mod):
206         import importlib
207         # reloading this causes internal errors
208         # because the classes from this module are stored internally
209         # possibly to refresh internal references too but for now, best not to.
210         if mod == _bpy_types:
211             return mod
212
213         try:
214             return importlib.reload(mod)
215         except:
216             import traceback
217             traceback.print_exc()
218
219     def test_register(mod):
220
221         if refresh_scripts and mod in original_modules:
222             return
223
224         if reload_scripts and mod:
225             print("Reloading:", mod)
226             mod = test_reload(mod)
227
228         if mod:
229             register_module_call(mod)
230             _global_loaded_modules.append(mod.__name__)
231
232     if reload_scripts:
233
234         # module names -> modules
235         _global_loaded_modules[:] = [_sys.modules[mod_name]
236                                      for mod_name in _global_loaded_modules]
237
238         # loop over and unload all scripts
239         _global_loaded_modules.reverse()
240         for mod in _global_loaded_modules:
241             unregister_module_call(mod)
242
243         for mod in _global_loaded_modules:
244             test_reload(mod)
245
246         del _global_loaded_modules[:]
247
248     from bpy_restrict_state import RestrictBlend
249
250     with RestrictBlend():
251         for base_path in script_paths(use_user=use_user):
252             for path_subdir in _script_module_dirs:
253                 path = _os.path.join(base_path, path_subdir)
254                 if _os.path.isdir(path):
255                     _sys_path_ensure(path)
256
257                     # only add this to sys.modules, don't run
258                     if path_subdir == "modules":
259                         continue
260
261                     for mod in modules_from_path(path, loaded_modules):
262                         test_register(mod)
263
264     # load template (if set)
265     if any(_bpy.utils.app_template_paths()):
266         import bl_app_template_utils
267         bl_app_template_utils.reset(reload_scripts=reload_scripts)
268         del bl_app_template_utils
269
270     # deal with addons separately
271     _initialize = getattr(_addon_utils, "_initialize", None)
272     if _initialize is not None:
273         # first time, use fast-path
274         _initialize()
275         del _addon_utils._initialize
276     else:
277         _addon_utils.reset_all(reload_scripts=reload_scripts)
278     del _initialize
279
280     if reload_scripts:
281         import gc
282         print("gc.collect() -> %d" % gc.collect())
283
284     if use_time:
285         print("Python Script Load Time %.4f" % (time.time() - t_main))
286
287     if use_class_register_check:
288         for cls in _bpy.types.bpy_struct.__subclasses__():
289             if getattr(cls, "is_registered", False):
290                 for subcls in cls.__subclasses__():
291                     if not subcls.is_registered:
292                         print(
293                             "Warning, unregistered class: %s(%s)" %
294                             (subcls.__name__, cls.__name__)
295                         )
296
297
298 # base scripts
299 _scripts = (
300     _os.path.dirname(_os.path.dirname(_os.path.dirname(__file__))),
301 )
302
303
304 def script_path_user():
305     """returns the env var and falls back to home dir or None"""
306     path = _user_resource('SCRIPTS')
307     return _os.path.normpath(path) if path else None
308
309
310 def script_path_pref():
311     """returns the user preference or None"""
312     path = _preferences.filepaths.script_directory
313     return _os.path.normpath(path) if path else None
314
315
316 def script_paths(subdir=None, user_pref=True, check_all=False, use_user=True):
317     """
318     Returns a list of valid script paths.
319
320     :arg subdir: Optional subdir.
321     :type subdir: string
322     :arg user_pref: Include the user preference script path.
323     :type user_pref: bool
324     :arg check_all: Include local, user and system paths rather just the paths
325        blender uses.
326     :type check_all: bool
327     :return: script paths.
328     :rtype: list
329     """
330     scripts = list(_scripts)
331
332     # Only paths Blender uses.
333     #
334     # Needed this is needed even when 'check_all' is enabled,
335     # so the 'BLENDER_SYSTEM_SCRIPTS' environment variable will be used.
336     base_paths = _bpy_script_paths()
337
338     # Defined to be (system, user) so we can skip the second if needed.
339     if not use_user:
340         base_paths = base_paths[:1]
341
342     if check_all:
343         # All possible paths, no duplicates, keep order.
344         if use_user:
345             test_paths = ('LOCAL', 'USER', 'SYSTEM')
346         else:
347             test_paths = ('LOCAL', 'SYSTEM')
348
349         base_paths = (
350             *(path for path in (
351                 _os.path.join(resource_path(res), "scripts")
352                 for res in test_paths) if path not in base_paths),
353             *base_paths,
354         )
355
356     if use_user:
357         test_paths = (*base_paths, script_path_user(), script_path_pref())
358     else:
359         test_paths = (*base_paths, script_path_pref())
360
361     for path in test_paths:
362         if path:
363             path = _os.path.normpath(path)
364             if path not in scripts and _os.path.isdir(path):
365                 scripts.append(path)
366
367     if subdir is None:
368         return scripts
369
370     scripts_subdir = []
371     for path in scripts:
372         path_subdir = _os.path.join(path, subdir)
373         if _os.path.isdir(path_subdir):
374             scripts_subdir.append(path_subdir)
375
376     return scripts_subdir
377
378
379 def refresh_script_paths():
380     """
381     Run this after creating new script paths to update sys.path
382     """
383
384     for base_path in script_paths():
385         for path_subdir in _script_module_dirs:
386             path = _os.path.join(base_path, path_subdir)
387             if _os.path.isdir(path):
388                 _sys_path_ensure(path)
389
390     for path in _addon_utils.paths():
391         _sys_path_ensure(path)
392         path = _os.path.join(path, "modules")
393         if _os.path.isdir(path):
394             _sys_path_ensure(path)
395
396
397 def app_template_paths(subdir=None):
398     """
399     Returns valid application template paths.
400
401     :arg subdir: Optional subdir.
402     :type subdir: string
403     :return: app template paths.
404     :rtype: generator
405     """
406     # Note: keep in sync with: Blender's BKE_appdir_app_template_any
407
408     subdir_tuple = (subdir,) if subdir is not None else ()
409
410     # Avoid adding 'bl_app_templates_system' twice.
411     # Either we have a portable build or an installed system build.
412     for resource_type, module_name in (
413             ('USER', "bl_app_templates_user"),
414             ('LOCAL', "bl_app_templates_system"),
415             ('SYSTEM', "bl_app_templates_system"),
416     ):
417         path = resource_path(resource_type)
418         if path:
419             path = _os.path.join(
420                 *(path, "scripts", "startup", module_name, *subdir_tuple))
421             if _os.path.isdir(path):
422                 yield path
423                 # Only load LOCAL or SYSTEM (never both).
424                 if resource_type == 'LOCAL':
425                     break
426
427
428 def preset_paths(subdir):
429     """
430     Returns a list of paths for a specific preset.
431
432     :arg subdir: preset subdirectory (must not be an absolute path).
433     :type subdir: string
434     :return: script paths.
435     :rtype: list
436     """
437     dirs = []
438     for path in script_paths("presets", check_all=True):
439         directory = _os.path.join(path, subdir)
440         if not directory.startswith(path):
441             raise Exception("invalid subdir given %r" % subdir)
442         elif _os.path.isdir(directory):
443             dirs.append(directory)
444
445     # Find addons preset paths
446     for path in _addon_utils.paths():
447         directory = _os.path.join(path, "presets", subdir)
448         if _os.path.isdir(directory):
449             dirs.append(directory)
450
451     return dirs
452
453
454 def smpte_from_seconds(time, fps=None):
455     """
456     Returns an SMPTE formatted string from the *time*:
457     ``HH:MM:SS:FF``.
458
459     If the *fps* is not given the current scene is used.
460
461     :arg time: time in seconds.
462     :type time: int, float or ``datetime.timedelta``.
463     :return: the frame string.
464     :rtype: string
465     """
466
467     return smpte_from_frame(time_to_frame(time, fps=fps), fps)
468
469
470 def smpte_from_frame(frame, fps=None, fps_base=None):
471     """
472     Returns an SMPTE formatted string from the *frame*:
473     ``HH:MM:SS:FF``.
474
475     If *fps* and *fps_base* are not given the current scene is used.
476
477     :arg frame: frame number.
478     :type frame: int or float.
479     :return: the frame string.
480     :rtype: string
481     """
482
483     if fps is None:
484         fps = _bpy.context.scene.render.fps
485
486     if fps_base is None:
487         fps_base = _bpy.context.scene.render.fps_base
488
489     sign = "-" if frame < 0 else ""
490     frame = abs(frame * fps_base)
491
492     return (
493         "%s%02d:%02d:%02d:%02d" % (
494             sign,
495             int(frame / (3600 * fps)),          # HH
496             int((frame / (60 * fps)) % 60),     # MM
497             int((frame / fps) % 60),            # SS
498             int(frame % fps),                   # FF
499         ))
500
501
502 def time_from_frame(frame, fps=None, fps_base=None):
503     """
504     Returns the time from a frame number .
505
506     If *fps* and *fps_base* are not given the current scene is used.
507
508     :arg frame: number.
509     :type frame: int or float.
510     :return: the time in seconds.
511     :rtype: datetime.timedelta
512     """
513
514     if fps is None:
515         fps = _bpy.context.scene.render.fps
516
517     if fps_base is None:
518         fps_base = _bpy.context.scene.render.fps_base
519
520     from datetime import timedelta
521
522     return timedelta(0, (frame * fps_base) / fps)
523
524
525 def time_to_frame(time, fps=None, fps_base=None):
526     """
527     Returns a float frame number from a time given in seconds or
528     as a datetime.timedelta object.
529
530     If *fps* and *fps_base* are not given the current scene is used.
531
532     :arg time: time in seconds.
533     :type time: number or a ``datetime.timedelta`` object
534     :return: the frame.
535     :rtype: float
536     """
537
538     if fps is None:
539         fps = _bpy.context.scene.render.fps
540
541     if fps_base is None:
542         fps_base = _bpy.context.scene.render.fps_base
543
544     from datetime import timedelta
545
546     if isinstance(time, timedelta):
547         time = time.total_seconds()
548
549     return (time / fps_base) * fps
550
551
552 def preset_find(name, preset_path, display_name=False, ext=".py"):
553     if not name:
554         return None
555
556     for directory in preset_paths(preset_path):
557
558         if display_name:
559             filename = ""
560             for fn in _os.listdir(directory):
561                 if fn.endswith(ext) and name == _bpy.path.display_name(fn):
562                     filename = fn
563                     break
564         else:
565             filename = name + ext
566
567         if filename:
568             filepath = _os.path.join(directory, filename)
569             if _os.path.exists(filepath):
570                 return filepath
571
572
573 def keyconfig_init():
574     # Key configuration initialization and refresh, called from the Blender
575     # window manager on startup and refresh.
576     active_config = _preferences.keymap.active_keyconfig
577
578     # Load the default key configuration.
579     default_filepath = preset_find("blender", "keyconfig")
580     keyconfig_set(default_filepath)
581
582     # Set the active key configuration if different
583     filepath = preset_find(active_config, "keyconfig")
584
585     if filepath and filepath != default_filepath:
586         keyconfig_set(filepath)
587
588
589 def keyconfig_set(filepath, report=None):
590     from os.path import basename, splitext
591
592     if _bpy.app.debug_python:
593         print("loading preset:", filepath)
594
595     keyconfigs = _bpy.context.window_manager.keyconfigs
596
597     try:
598         error_msg = ""
599         execfile(filepath)
600     except:
601         import traceback
602         error_msg = traceback.format_exc()
603
604     name = splitext(basename(filepath))[0]
605     kc_new = keyconfigs.get(name)
606
607     if error_msg:
608         if report is not None:
609             report({'ERROR'}, error_msg)
610         print(error_msg)
611         if kc_new is not None:
612             keyconfigs.remove(kc_new)
613         return False
614
615     # Get name, exception for default keymap to keep backwards compatibility.
616     if kc_new is None:
617         if report is not None:
618             report({'ERROR'}, "Failed to load keymap %r" % filepath)
619         return False
620     else:
621         keyconfigs.active = kc_new
622         return True
623
624
625 def user_resource(resource_type, path="", create=False):
626     """
627     Return a user resource path (normally from the users home directory).
628
629     :arg type: Resource type in ['DATAFILES', 'CONFIG', 'SCRIPTS', 'AUTOSAVE'].
630     :type type: string
631     :arg subdir: Optional subdirectory.
632     :type subdir: string
633     :arg create: Treat the path as a directory and create
634        it if its not existing.
635     :type create: boolean
636     :return: a path.
637     :rtype: string
638     """
639
640     target_path = _user_resource(resource_type, path)
641
642     if create:
643         # should always be true.
644         if target_path:
645             # create path if not existing.
646             if not _os.path.exists(target_path):
647                 try:
648                     _os.makedirs(target_path)
649                 except:
650                     import traceback
651                     traceback.print_exc()
652                     target_path = ""
653             elif not _os.path.isdir(target_path):
654                 print("Path %r found but isn't a directory!" % target_path)
655                 target_path = ""
656
657     return target_path
658
659
660 def register_classes_factory(classes):
661     """
662     Utility function to create register and unregister functions
663     which simply registers and unregisters a sequence of classes.
664     """
665     def register():
666         from bpy.utils import register_class
667         for cls in classes:
668             register_class(cls)
669
670     def unregister():
671         from bpy.utils import unregister_class
672         for cls in reversed(classes):
673             unregister_class(cls)
674
675     return register, unregister
676
677
678 def register_submodule_factory(module_name, submodule_names):
679     """
680     Utility function to create register and unregister functions
681     which simply load submodules,
682     calling their register & unregister functions.
683
684     .. note::
685
686        Modules are registered in the order given,
687        unregistered in reverse order.
688
689     :arg module_name: The module name, typically ``__name__``.
690     :type module_name: string
691     :arg submodule_names: List of submodule names to load and unload.
692     :type submodule_names: list of strings
693     :return: register and unregister functions.
694     :rtype: tuple pair of functions
695     """
696
697     module = None
698     submodules = []
699
700     def register():
701         nonlocal module
702         module = __import__(name=module_name, fromlist=submodule_names)
703         submodules[:] = [getattr(module, name) for name in submodule_names]
704         for mod in submodules:
705             mod.register()
706
707     def unregister():
708         from sys import modules
709         for mod in reversed(submodules):
710             mod.unregister()
711             name = mod.__name__
712             delattr(module, name.partition(".")[2])
713             del modules[name]
714         submodules.clear()
715
716     return register, unregister
717
718
719 # -----------------------------------------------------------------------------
720 # Tool Registration
721
722
723 def register_tool(tool_cls, *, after=None, separator=False, group=False):
724     """
725     Register a tool in the toolbar.
726
727     :arg tool: A tool subclass.
728     :type tool: :class:`bpy.types.WorkSpaceTool` subclass.
729     :arg space_type: Space type identifier.
730     :type space_type: string
731     :arg after: Optional identifiers this tool will be added after.
732     :type after: collection of strings or None.
733     :arg separator: When true, add a separator before this tool.
734     :type separator: bool
735     :arg group: When true, add a new nested group of tools.
736     :type group: bool
737     """
738     space_type = tool_cls.bl_space_type
739     context_mode = tool_cls.bl_context_mode
740
741     from bl_ui.space_toolsystem_common import (
742         ToolSelectPanelHelper,
743         ToolDef,
744     )
745
746     cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
747     if cls is None:
748         raise Exception(f"Space type {space_type!r} has no toolbar")
749     tools = cls._tools[context_mode]
750
751     # First sanity check
752     from bpy.types import WorkSpaceTool
753     tools_id = {
754         item.idname for item in ToolSelectPanelHelper._tools_flatten(tools)
755         if item is not None
756     }
757     if not issubclass(tool_cls, WorkSpaceTool):
758         raise Exception(f"Expected WorkSpaceTool subclass, not {type(tool_cls)!r}")
759     if tool_cls.bl_idname in tools_id:
760         raise Exception(f"Tool {tool_cls.bl_idname!r} already exists!")
761     del tools_id, WorkSpaceTool
762
763     # Convert the class into a ToolDef.
764     def tool_from_class(tool_cls):
765         # Convert class to tuple, store in the class for removal.
766         tool_def = ToolDef.from_dict({
767             "idname": tool_cls.bl_idname,
768             "label": tool_cls.bl_label,
769             "description": getattr(tool_cls, "bl_description", tool_cls.__doc__),
770             "icon": getattr(tool_cls, "bl_icon", None),
771             "cursor": getattr(tool_cls, "bl_cursor", None),
772             "widget": getattr(tool_cls, "bl_widget", None),
773             "keymap": getattr(tool_cls, "bl_keymap", None),
774             "data_block": getattr(tool_cls, "bl_data_block", None),
775             "operator": getattr(tool_cls, "bl_operator", None),
776             "draw_settings": getattr(tool_cls, "draw_settings", None),
777             "draw_cursor": getattr(tool_cls, "draw_cursor", None),
778         })
779         tool_cls._bl_tool = tool_def
780
781         keymap_data = tool_def.keymap
782         if keymap_data is not None:
783             if context_mode is None:
784                 context_descr = "All"
785             else:
786                 context_descr = context_mode.replace("_", " ").title()
787             from bpy import context
788             wm = context.window_manager
789             kc = wm.keyconfigs.default
790             if callable(keymap_data[0]):
791                 cls._km_action_simple(kc, context_descr, tool_def.label, keymap_data)
792         return tool_def
793
794     tool_converted = tool_from_class(tool_cls)
795
796     if group:
797         # Create a new group
798         tool_converted = (tool_converted,)
799
800
801     tool_def_insert = (
802         (None, tool_converted) if separator else
803         (tool_converted,)
804     )
805
806     def skip_to_end_of_group(seq, i):
807         i_prev = i
808         while i < len(seq) and seq[i] is not None:
809             i_prev = i
810             i += 1
811         return i_prev
812
813     changed = False
814     if after is not None:
815         for i, item in enumerate(tools):
816             if item is None:
817                 pass
818             elif isinstance(item, ToolDef):
819                 if item.idname in after:
820                     i = skip_to_end_of_group(item, i)
821                     tools[i + 1:i + 1] = tool_def_insert
822                     changed = True
823                     break
824             elif isinstance(item, tuple):
825                 for j, sub_item in enumerate(item, 1):
826                     if isinstance(sub_item, ToolDef):
827                         if sub_item.idname in after:
828                             if group:
829                                 # Can't add a group within a group,
830                                 # add a new group after this group.
831                                 i = skip_to_end_of_group(tools, i)
832                                 tools[i + 1:i + 1] = tool_def_insert
833                             else:
834                                 j = skip_to_end_of_group(item, j)
835                                 item = item[:j + 1] + tool_def_insert + item[j + 1:]
836                                 tools[i] = item
837                             changed = True
838                             break
839                 if changed:
840                     break
841
842         if not changed:
843             print("bpy.utils.register_tool: could not find 'after'", after)
844     if not changed:
845         tools.extend(tool_def_insert)
846
847
848 def unregister_tool(tool_cls):
849     space_type = tool_cls.bl_space_type
850     context_mode = tool_cls.bl_context_mode
851
852     from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
853     cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
854     if cls is None:
855         raise Exception(f"Space type {space_type!r} has no toolbar")
856     tools = cls._tools[context_mode]
857
858     tool_def = tool_cls._bl_tool
859     try:
860         i = tools.index(tool_def)
861     except ValueError:
862         i = -1
863
864     def tool_list_clean(tool_list):
865         # Trim separators.
866         while tool_list and tool_list[-1] is None:
867             del tool_list[-1]
868         while tool_list and tool_list[0] is None:
869             del tool_list[0]
870         # Remove duplicate separators.
871         for i in range(len(tool_list) - 1, -1, -1):
872             is_none = tool_list[i] is None
873             if is_none and prev_is_none:
874                 del tool_list[i]
875             prev_is_none = is_none
876
877     changed = False
878     if i != -1:
879         del tools[i]
880         tool_list_clean(tools)
881         changed = True
882
883     if not changed:
884         for i, item in enumerate(tools):
885             if isinstance(item, tuple):
886                 try:
887                     j = item.index(tool_def)
888                 except ValueError:
889                     j = -1
890
891                 if j != -1:
892                     item_clean = list(item)
893                     del item_clean[j]
894                     tool_list_clean(item_clean)
895                     if item_clean:
896                         tools[i] = tuple(item_clean)
897                     else:
898                         del tools[i]
899                         tool_list_clean(tools)
900                     del item_clean
901
902                     # tuple(sub_item for sub_item in items if sub_item is not tool_def)
903                     changed = True
904                     break
905
906     if not changed:
907         raise Exception(f"Unable to remove {tool_cls!r}")
908     del tool_cls._bl_tool
909
910     keymap_data = tool_def.keymap
911     if keymap_data is not None:
912         from bpy import context
913         wm = context.window_manager
914         kc = wm.keyconfigs.default
915         km = kc.keymaps.get(keymap_data[0])
916         if km is None:
917             print("Warning keymap {keymap_data[0]!r} not found!")
918         else:
919             kc.keymaps.remove(km)
920
921
922 # -----------------------------------------------------------------------------
923 # Manual lookups, each function has to return a basepath and a sequence
924 # of...
925
926 # we start with the built-in default mapping
927 def _blender_default_map():
928     import rna_manual_reference as ref_mod
929     ret = (ref_mod.url_manual_prefix, ref_mod.url_manual_mapping)
930     # avoid storing in memory
931     del _sys.modules["rna_manual_reference"]
932     return ret
933
934
935 # hooks for doc lookups
936 _manual_map = [_blender_default_map]
937
938
939 def register_manual_map(manual_hook):
940     _manual_map.append(manual_hook)
941
942
943 def unregister_manual_map(manual_hook):
944     _manual_map.remove(manual_hook)
945
946
947 def manual_map():
948     # reverse so default is called last
949     for cb in reversed(_manual_map):
950         try:
951             prefix, url_manual_mapping = cb()
952         except:
953             print("Error calling %r" % cb)
954             import traceback
955             traceback.print_exc()
956             continue
957
958         yield prefix, url_manual_mapping
959
960
961 # Build an RNA path from struct/property/enum names.
962 def make_rna_paths(struct_name, prop_name, enum_name):
963     """
964     Create RNA "paths" from given names.
965
966     :arg struct_name: Name of a RNA struct (like e.g. "Scene").
967     :type struct_name: string
968     :arg prop_name: Name of a RNA struct's property.
969     :type prop_name: string
970     :arg enum_name: Name of a RNA enum identifier.
971     :type enum_name: string
972     :return: A triple of three "RNA paths"
973        (most_complete_path, "struct.prop", "struct.prop:'enum'").
974        If no enum_name is given, the third element will always be void.
975     :rtype: tuple of strings
976     """
977     src = src_rna = src_enum = ""
978     if struct_name:
979         if prop_name:
980             src = src_rna = ".".join((struct_name, prop_name))
981             if enum_name:
982                 src = src_enum = "%s:'%s'" % (src_rna, enum_name)
983         else:
984             src = src_rna = struct_name
985     return src, src_rna, src_enum