diff --git a/__init__.py b/__init__.py index 5888c7e..0420051 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,4 @@ -from . import preferences, public -from .tool.addon_search import remember_expanded - +from . import registration bl_info = { "name": "Development Kit Tool", "version": (1, 0, 1), @@ -14,11 +12,8 @@ def register(): - public.PublicClass.clear_cache() - preferences.register() + registration.register() def unregister(): - remember_expanded() - public.PublicClass.clear_cache() - preferences.unregister() + registration.unregister() diff --git a/keymap.py b/keymap.py new file mode 100644 index 0000000..12623bd --- /dev/null +++ b/keymap.py @@ -0,0 +1,87 @@ +import bpy + +from mathutils import Vector, Euler, Matrix + + +def get_keymap(keymap_name) -> "bpy.types.KeyMap": + kc = bpy.context.window_manager.keyconfigs + addon = kc.addon + keymap = kc.default.keymaps.get(keymap_name, None) + return addon.keymaps.new( + keymap_name, + space_type=keymap.space_type, + region_type=keymap.region_type + ) + + +def get_kmi_operator_properties(kmi: 'bpy.types.KeyMapItem') -> dict: + """获取kmi操作符的属性""" + properties = kmi.properties + prop_keys = dict(properties.items()).keys() + dictionary = {i: getattr(properties, i, None) for i in prop_keys} + del_key = [] + for item in dictionary: + prop = getattr(properties, item, None) + typ = type(prop) + if prop: + if typ == Vector: + # 属性阵列-浮点数组 + dictionary[item] = dictionary[item].to_tuple() + elif typ == Euler: + dictionary[item] = dictionary[item][:] + elif typ == Matrix: + dictionary[item] = tuple(i[:] for i in dictionary[item]) + elif typ == bpy.types.bpy_prop_array: + dictionary[item] = dictionary[item][:] + elif typ in (str, bool, float, int, set, list, tuple): + ... + elif typ.__name__ in [ + 'TRANSFORM_OT_shrink_fatten', + 'TRANSFORM_OT_translate', + 'TRANSFORM_OT_edge_slide', + 'NLA_OT_duplicate', + 'ACTION_OT_duplicate', + 'GRAPH_OT_duplicate', + 'TRANSFORM_OT_translate', + 'OBJECT_OT_duplicate', + 'MESH_OT_loopcut', + 'MESH_OT_rip_edge', + 'MESH_OT_rip', + 'MESH_OT_duplicate', + 'MESH_OT_offset_edge_loops', + 'MESH_OT_extrude_faces_indiv', + ]: # 一些奇怪的操作符属性,不太好解析也用不上 + ... + del_key.append(item) + else: + print('emm 未知属性,', typ, dictionary[item]) + del_key.append(item) + for i in del_key: + dictionary.pop(i) + return dictionary + + +def draw_key(layout: bpy.types.UILayout): + """在偏好设置绘制插件快捷键""" + import rna_keymap_ui + from .tool.development_key import register_keymap_items + + kc = bpy.context.window_manager.keyconfigs.user + + for km, kmi in register_keymap_items: + kmm = kc.keymaps.get(km.name) + if kmm: + is_find = False + for kmii in kmm.keymap_items: + if kmi.idname == kmii.idname: + if get_kmi_operator_properties(kmi) == get_kmi_operator_properties(kmii): + col = layout.column(align=True) + if (not kmii.is_user_defined) and kmii.is_user_modified: + col.context_pointer_set("keymap", kmm) + is_find = True + rna_keymap_ui.draw_kmi(["ADDON", "USER", "DEFAULT"], kc, kmm, kmii, col, 0) + break # 找到了,但有可能会找到多个 + if not is_find: + column = layout.column(align=True) + column.label(text="Not Found Keymap, Please check the shortcut keys that have been changed") + draw_restore_keymap_button(column) diff --git a/preferences.py b/preferences.py index 29f0ceb..63f5b8b 100644 --- a/preferences.py +++ b/preferences.py @@ -1,130 +1,88 @@ -import bpy.utils -from bpy.props import BoolProperty, IntProperty, StringProperty, CollectionProperty -from bpy.types import AddonPreferences +import bpy -from .public import PublicClass -from .tool import (auto_reload_script, - custom_key, - fast_open_addon_code, - restart_blender, - addon_search, - ) +from .tool import update_by_tool_name +from .tool.auto_reload_script import AutoReloadScriptPreferences -tool_mod = {'fast_open_addon_code': fast_open_addon_code, - 'enabled_reload_script': auto_reload_script, - 'restart_blender': restart_blender, - 'custom_key': custom_key, - 'save_addon_search': addon_search - } +class ShowExpandedItem(bpy.types.PropertyGroup): + """Used to record the expansion status of the addon""" + show_expanded: bpy.props.BoolProperty(default=False) -def update_tool(un_register=False): - pref = PublicClass.pref_() - for prop_name, tool in tool_mod.items(): - is_enable = getattr(pref, prop_name, False) - if un_register or (not is_enable): - tool.unregister() - elif is_enable: - tool.register() - -class ShowExpanded(bpy.types.PropertyGroup): - show_expanded: BoolProperty(default=False) - - -class ToolPreferences(AddonPreferences): +class ToolPreferences(bpy.types.AddonPreferences, AutoReloadScriptPreferences): bl_idname = __package__ - @staticmethod - def update_by_tool_name(tool_name): - """Change prop update tool""" - - def update(self, context): - prop = getattr(self, tool_name, None) - if prop: - tool_mod[tool_name].register() - elif prop is False: - tool_mod[tool_name].unregister() + activate_auto_reload_script: bpy.props.BoolProperty( + default=True, + name="ReLoad Script", + description="Automatically reload scripts and run them", + update=update_by_tool_name("auto_reload_script"), + ) - return update + activate_development_key: bpy.props.BoolProperty( + default=True, + name="Development Keymap", + description="Commonly used Keymaps to speed up the development process", + update=update_by_tool_name("development_key"), + ) - fast_open_addon_code: BoolProperty( + activate_open_addon_script: bpy.props.BoolProperty( default=False, - name='Feat Open Addon Script or Folder', - description='Rewrite the drawing method of the addon section,' - ' and display it in the expansion of the addon', - update=update_by_tool_name('fast_open_addon_code'), ) - restart_blender: BoolProperty( - default=True, - name='Restart Blender', - description='Enabled Multiple Blender,or Restart Blender', - update=update_by_tool_name('restart_blender'), + name="Addon Open", + description="Rewrite the drawing method of the addon section, and display it in the expansion of the addon", + update=update_by_tool_name("open_addon_script") ) - custom_key: BoolProperty( + activate_remember_addon_expanded: bpy.props.BoolProperty( default=True, - name='Development Key', - description='alt+Space Toggle Full Screen' - 'ctrl+alt+MiddleMouse Show Console' - 'ctrl+alt+RightMouse Switch User Translate Interface' - 'ctrl+alt+AccentGrave Save Home File', - update=update_by_tool_name('custom_key'), + name="Remember addon expanded", + description="Record the expanded Addon and restore it the next time you open Blender", + update=update_by_tool_name("remember_addon_expanded"), ) - save_addon_search: BoolProperty( + + activate_remember_addon_search: bpy.props.BoolProperty( default=True, - name='Save addon search', - description='', - update=update_by_tool_name('save_addon_search'), + name="Remember addon search", + description="Record the Addon search and restore it the next time you start Blender", + update=update_by_tool_name("remember_addon_search"), ) - addon_search: StringProperty() - addon_show_expanded: CollectionProperty(type=ShowExpanded) - enabled_reload_script: BoolProperty( + activate_restart_blender: bpy.props.BoolProperty( default=True, - name='ReLoad Script Tool', - description='', - update=update_by_tool_name('enabled_reload_script'), + name="Restart Blender", + description="Enable multiple Blenders or restart Blender, please be careful to save the edit file!!!!", + update=update_by_tool_name("restart_blender"), ) - # Auto Reload - def update_reload_script(self, context): - text = context.space_data.text - try: - bpy.ops.text.reload() - if self.auto_run_script: - try: - bpy.ops.text.run_script() - print(f'Reload Script {text.name},and Run Script!!!') - except Exception as e: - print('Run Error!!', e.args) - except Exception as e: - print(f'Reload Script {text.name} Error,Perhaps this script does not exist', e.args) - self.auto_reload_script = False - - reload_script_number: IntProperty(default=True, - update=update_reload_script) - auto_run_script: BoolProperty(name='Auto run script switch, only when auto reload script is turned on can it run', - options={'SKIP_SAVE'}, - default=False) - - auto_reload_script: BoolProperty(name="Whether to automatically reload scripts", default=True, ) + # Other Property + addon_show_expanded: bpy.props.CollectionProperty(type=ShowExpandedItem) + addon_search: bpy.props.StringProperty(default="") def draw(self, context): - for i in ('fast_open_addon_code', - 'enabled_reload_script', - 'restart_blender', - 'custom_key', - 'save_addon_search', - ): - self.layout.prop(self, i) + from .keymap import draw_key + + column = self.layout.column(align=True) + for prop in self.bl_rna.properties: + if prop.identifier.startswith("activate_"): + self.draw_prop(column, prop.identifier) + + if self.activate_development_key: + column.separator() + col = column.box().column(align=True) + col.label(text="Keymap") + draw_key(col) + + def draw_prop(self, layout, identifier) -> None: + split = layout.row(align=True).split(factor=.2, align=True) + prop = self.bl_rna.properties[identifier] + split.prop(self, identifier, toggle=True, expand=True) + split.label(text=prop.description) def register(): - bpy.utils.register_class(ShowExpanded) + bpy.utils.register_class(ShowExpandedItem) bpy.utils.register_class(ToolPreferences) - update_tool() def unregister(): bpy.utils.unregister_class(ToolPreferences) - bpy.utils.unregister_class(ShowExpanded) - update_tool(un_register=True) + bpy.utils.unregister_class(ShowExpandedItem) diff --git a/public.py b/public.py deleted file mode 100644 index cd296e8..0000000 --- a/public.py +++ /dev/null @@ -1,99 +0,0 @@ -import os -from functools import cache - -import bpy - - -def get_pref(): - return bpy.context.preferences.addons[__package__].preferences - - -class PublicEvent: - not_key: bool - only_ctrl: bool - only_alt: bool - only_shift: bool - shift_alt: bool - ctrl_alt: bool - ctrl_shift: bool - ctrl_shift_alt: bool - - @staticmethod - def get_event_key(event): - alt = event.alt - shift = event.shift - ctrl = event.ctrl - - not_key = ((not ctrl) and (not alt) and (not shift)) - - only_ctrl = (ctrl and (not alt) and (not shift)) - only_alt = ((not ctrl) and alt and (not shift)) - only_shift = ((not ctrl) and (not alt) and shift) - - shift_alt = ((not ctrl) and alt and shift) - ctrl_alt = (ctrl and alt and (not shift)) - - ctrl_shift = (ctrl and (not alt) and shift) - ctrl_shift_alt = (ctrl and alt and shift) - return not_key, only_ctrl, only_alt, only_shift, shift_alt, ctrl_alt, ctrl_shift, ctrl_shift_alt - - def set_event_key(self, event): - self.not_key, self.only_ctrl, self.only_alt, self.only_shift, self.shift_alt, self.ctrl_alt, self.ctrl_shift, self.ctrl_shift_alt = \ - self.get_event_key(event) - - -class PublicPref: - - @staticmethod - @cache - def pref_(): - return get_pref() - - @property - def pref(self): - return self.pref_() - - -class PublicClass(PublicEvent, - PublicPref, - ): - - @staticmethod - def clear_cache(): - PublicClass.pref_.cache_clear() - - @staticmethod - def get_addon_is_enabled(module_name, rsc_type=None): - if rsc_type is None: - return module_name in {ext.module for ext in bpy.context.preferences.addons} - elif rsc_type == 'str': - return f"'{module_name}'" + " in {ext.module for ext in bpy.context.preferences.addons}" - - @staticmethod - def get_addon_user_dirs(): - version = bpy.app.version - if version >= (3, 6, 0): # 4.0以上 - addon_user_dirs = tuple( - p for p in ( - *[os.path.join(pref_p, "addons") for pref_p in bpy.utils.script_paths_pref()], - bpy.utils.user_resource('SCRIPTS', path="addons"), - ) - if p - ) - elif bpy.app.version >= (2, 94, 0): # 3.0 version - addon_user_dirs = tuple( - p for p in ( - os.path.join(bpy.context.preferences.filepaths.script_directory, "addons"), - bpy.utils.user_resource('SCRIPTS', path="addons"), - ) - if p - ) - else: # 2.93 version - addon_user_dirs = tuple( - p for p in ( - os.path.join(bpy.context.preferences.filepaths.script_directory, "addons"), - bpy.utils.user_resource('SCRIPTS', "addons"), - ) - if p - ) - return addon_user_dirs diff --git a/registration.py b/registration.py new file mode 100644 index 0000000..1d7ccd0 --- /dev/null +++ b/registration.py @@ -0,0 +1,38 @@ +from . import preferences +from . import tool +from . import translation +from .tool.remember_addon_expanded import restore_addons_expanded +from .tool.remember_addon_search import restore_addon_search +from .utils import clear_cache + +DEVELOPMENT_KEY_MAPS = { + "Window": [ + {"idname": "wm.window_fullscreen_toggle", "type": "SPACE", "value": "PRESS", "alt": True}, + {"idname": "wm.console_toggle", "type": "MIDDLEMOUSE", "value": "PRESS", "ctrl": True, "alt": True}, + {"idname": "wm.save_homefile", "type": "ACCENT_GRAVE", "value": "PRESS", "ctrl": True, "alt": True}, + {"idname": "wm.context_toggle", "type": "RIGHTMOUSE", "value": "PRESS", "ctrl": True, "alt": True, + "properties": {"data_path": "preferences.view.use_translate_interface"} + }, + ], + "Screen": [ + {"idname": "screen.userpref_show", "type": "U", "value": "PRESS", "ctrl": True, "alt": True}, + {"idname": "screen.region_flip", "type": "RIGHTMOUSE", "value": "PRESS", "shift": True, "ctrl": True} + ] +} + + +def register(): + clear_cache() + preferences.register() + tool.register() + + restore_addon_search() + restore_addons_expanded() + translation.register() + + +def unregister(): + clear_cache() + tool.unregister() + preferences.unregister() + translation.unregister() diff --git a/tool/__init__.py b/tool/__init__.py new file mode 100644 index 0000000..042b89d --- /dev/null +++ b/tool/__init__.py @@ -0,0 +1,59 @@ +from . import ( + auto_reload_script, + development_key, + open_addon_script, + remember_addon_expanded, + remember_addon_search, + restart_blender, +) + +tool_mods = { + auto_reload_script, + development_key, + open_addon_script, + remember_addon_expanded, + remember_addon_search, + restart_blender, +} + + +def update_tool(unregister_all=False): + from ..utils import get_pref + pref = get_pref() + + for tool in tool_mods: + name = "activate_" + tool.__name__.split(".")[-1] + is_enable = getattr(pref, name, False) + if unregister_all or (not is_enable): + """ + It is necessary to ensure that the module will not encounter errors + when calling unregister even if it is not registered + """ + tool.unregister() + elif is_enable: + tool.register() + + +def update_by_tool_name(tool_name: str): + """Change prop update tool""" + + def update(self, context): + prop = getattr(self, f"activate_{tool_name}", None) + for tool in tool_mods: + name = tool.__name__.split(".")[-1] + if name == tool_name: + if prop: + tool.register() + elif prop is False: + tool.unregister() + return + + return update + + +def register(): + update_tool() + + +def unregister(): + update_tool(unregister_all=True) diff --git a/tool/addon_search.py b/tool/addon_search.py deleted file mode 100644 index 4a931f6..0000000 --- a/tool/addon_search.py +++ /dev/null @@ -1,75 +0,0 @@ -import bpy -from bpy.app.handlers import persistent - -from ..public import PublicPref - -owner = object() - - -def msgbus_callback(): - PublicPref.pref_().addon_search = bpy.context.window_manager.addon_search - - -def msgbus(): - bpy.msgbus.subscribe_rna( - key=(bpy.types.WindowManager, "addon_search"), - owner=owner, - args=(), - notify=msgbus_callback, - ) - - -@persistent -def load_post(self, context): - msgbus() - - -def addon_keys(): - """获取插件keys""" - import addon_utils - if bpy.app.version >= (4, 2, 0): - return addon_utils.modules().mapping.keys() - else: - return [addon.__name__ for addon in addon_utils.modules()] - - -def set_addon_search(): - bpy.context.window_manager.addon_search = PublicPref.pref_().addon_search - - addon_show_expanded = PublicPref.pref_().addon_show_expanded - expanded_list = [addon.name for addon in addon_show_expanded if addon.show_expanded] - - import addon_utils - addon_utils.modules_refresh() - for key in addon_keys(): - if key in expanded_list: - bpy.ops.preferences.addon_expand(module=key) - - for area in bpy.context.screen.areas: - area.tag_redraw() - - -def remember_expanded(): - addon_show_expanded = PublicPref.pref_().addon_show_expanded - import addon_utils - addon_utils.modules_refresh() - for key, mod in addon_utils.modules().mapping.items(): - info = addon_utils.module_bl_info(mod) - show_expanded = info.get("show_expanded", False) - - i = addon_show_expanded.get(key, None) - if i is None: - i = addon_show_expanded.add() - i.name = key - i.show_expanded = show_expanded - bpy.ops.wm.save_userpref() - - -def register(): - msgbus() - bpy.app.handlers.load_post.append(load_post) - bpy.app.timers.register(set_addon_search, first_interval=1, persistent=True) - - -def unregister(): - bpy.msgbus.clear_by_owner(owner) diff --git a/tool/auto_reload_script.py b/tool/auto_reload_script.py index df13efe..a9c7e3c 100644 --- a/tool/auto_reload_script.py +++ b/tool/auto_reload_script.py @@ -1,72 +1,98 @@ -from os.path import dirname +import os import bpy -from bpy.types import Operator, Text, TEXT_HT_footer -from ..public import PublicClass +from ..utils import get_pref -class UnlinkText(Operator): - bl_idname = 'script.unlink_all' - bl_label = 'Unlink All Script' - bl_options = {'REGISTER', 'UNDO'} +class UnlinkAllScript(bpy.types.Operator): + bl_idname = "script.unlink_all" + bl_label = "Unlink All Script" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): while bpy.data.texts: - for text in bpy.data.texts: - print(text.name) - bpy.ops.text.unlink() - return {'FINISHED'} + bpy.ops.text.unlink() + return {"FINISHED"} -class ScriptingTools(Text): - text: Text = None +def draw_text_header(self, context): + pref = get_pref() - @classmethod - def register(cls): - TEXT_HT_footer.prepend(cls.draw_text_header) - bpy.utils.register_class(UnlinkText) + row = self.layout.row(align=True) - @classmethod - def unregister(cls): - TEXT_HT_footer.remove(cls.draw_text_header) - bpy.utils.unregister_class(UnlinkText) + text = context.space_data.text - def draw_text_header(self, context): - pref = PublicClass.pref_() - layout = self.layout + if text: + if not text.library: + row.prop( + pref, + "auto_reload_script", + text="", + toggle=True, + icon="FILE_REFRESH") + row.prop( + pref, + "auto_run_script", + text="", + toggle=True, + icon="PLAY") + + path_file = text.filepath + path_folder = os.path.dirname(path_file) + + row.operator("wm.path_open", text="", icon="FILE_SCRIPT").filepath = fr"{path_file}" + row.operator("wm.path_open", text="", icon="FILE_FOLDER").filepath = fr"{path_folder}" + + alert_row = row.row(align=True) + alert_row.alert = True + alert_row.operator(UnlinkAllScript.bl_idname, text="", icon="PANEL_CLOSE") + + if pref.auto_reload_script and text and text.is_modified: + # Judging whether the text has been modified through UI drawing + pref.reload_script_number += 1 + else: + row.label(text="Not Load or Select Script") + + +class AutoReloadScriptPreferences: + def update_reload_script(self, context): text = context.space_data.text - row = layout.row(align=True) - if text: - if not text.library: - row.prop(pref, 'auto_reload_script', - text='', - toggle=True, - icon='FILE_REFRESH') - row.prop(pref, 'auto_run_script', - text='', - toggle=True, - icon='PLAY') - - file_path = text.filepath - folder_path = dirname(file_path) - - row.operator('wm.path_open', text='', icon='FILE_SCRIPT').filepath = fr'{file_path}' - row.operator('wm.path_open', text='', icon='FILE_FOLDER').filepath = fr'{folder_path}' - - alert_row = row.row(align=True) - alert_row.alert = True - alert_row.operator(UnlinkText.bl_idname, text='', icon='PANEL_CLOSE') - - if pref.auto_reload_script and text and text.is_modified: - pref.reload_script_number += 1 - else: - row.label(text='Not Load or Select Script') + try: + bpy.ops.text.reload() + if self.auto_run_script: + try: + bpy.ops.text.run_script() + print(f"Reload Script {text.name},and Run Script!!!") + except Exception as e: + import traceback + traceback.print_exc() + traceback.print_stack() + print("Run Error!!", e.args) + except Exception as e: + print(f"Reload Script {text.name} Error,Perhaps this script does not exist", e.args) + self.auto_reload_script = False + + reload_script_number: bpy.props.IntProperty(default=-1, update=update_reload_script) + auto_run_script: bpy.props.BoolProperty( + name="Auto Run Script", + description="Auto run script switch, only when auto reload script is turned on can it run", + options={"SKIP_SAVE"}, + default=False) + + auto_reload_script: bpy.props.BoolProperty( + name="Auto Reload Script", + description="Whether to automatically reload scripts", + default=True, + ) def register(): - ScriptingTools.register() + bpy.utils.register_class(UnlinkAllScript) + bpy.types.TEXT_HT_footer.prepend(draw_text_header) def unregister(): - ScriptingTools.unregister() + bpy.types.TEXT_HT_footer.remove(draw_text_header) + if getattr(UnlinkAllScript, "is_registered", False): + bpy.utils.unregister_class(UnlinkAllScript) diff --git a/tool/custom_key.py b/tool/custom_key.py deleted file mode 100644 index f082dbe..0000000 --- a/tool/custom_key.py +++ /dev/null @@ -1,140 +0,0 @@ -import bpy - -from ..public import PublicClass - - -def set_key(): - pref = bpy.context.window_manager.keyconfigs['Blender'].preferences - pref.use_select_all_toggle = True # 使用全选切换 - pref.use_alt_click_leader = True # 使用ALT点击工具提示 - pref.use_pie_click_drag = True # 拖动显示饼菜单 - pref.use_use_v3d_shade_ex_pie = True # 额外着色饼菜单 - pref.use_use_v3d_tab_menu = True # 饼菜单选项卡 - pref.use_file_single_click = True # 饼菜单选项卡 - pref.spacebar_action = 'TOOL' - - -def set_m3_prop(): - if PublicClass.get_addon_is_enabled('MACHIN3tools'): - print('MACHIN3tools') - m3 = PublicClass.get_addon_is_enabled('MACHIN3tools') - tool_list = [ - 'activate_smart_vert', - 'activate_smart_edge', - 'activate_smart_face', - 'activate_clean_up', - 'activate_clipping_toggle', - 'activate_align', - 'activate_apply', - 'activate_select', - 'activate_mesh_cut', - 'activate_surface_slide', - 'activate_filebrowser_tools', - 'activate_smart_drive', - 'activate_unity', - 'activate_material_picker', - 'activate_group', - 'activate_thread', - 'activate_spin', - 'activate_smooth', - 'activate_save_pie', - 'activate_shading_pie', - 'activate_views_pie', - 'activate_align_pie', - 'activate_cursor_pie', - 'activate_transform_pie', - 'activate_snapping_pie', - 'activate_collections_pie', - 'activate_workspace_pie', - - # 'activate_mirror', - # 'activate_tools_pie', - # 'activate_customize', - # 'activate_focus', - # 'activate_modes_pie', - - 'snap_show_absolute_grid', - 'cursor_show_to_grid', - ] - m3_pref = 'bpy.context.preferences.addons["MACHIN3tools"].preferences' - - for i in tool_list: - if hasattr(m3, i) and (not eval(f'{m3_pref}.{i}')): - exec(f'{m3_pref}.{i} = {True}', ) - m3.cursor_set_transform_preset = False - - -class CustomKey: - KEY_MAPS = { - 'Window': [ - {'idname': 'wm.window_fullscreen_toggle', 'type': 'SPACE', 'value': 'PRESS', 'alt': True}, - {'idname': 'wm.console_toggle', 'type': 'MIDDLEMOUSE', 'value': 'PRESS', 'ctrl': True, 'alt': True}, - {'idname': 'wm.save_homefile', 'type': 'ACCENT_GRAVE', 'value': 'PRESS', 'ctrl': True, 'alt': True}, - {'idname': 'wm.context_toggle', - 'type': 'RIGHTMOUSE', - 'value': 'PRESS', - 'ctrl': True, - 'alt': True, - 'properties': [ - ('data_path', 'preferences.view.use_translate_interface'), - ] - }, - ], - 'Screen': [ - {'idname': 'screen.userpref_show', 'type': 'U', 'value': 'PRESS', 'ctrl': True, 'alt': True}, - {'idname': 'screen.region_flip', 'type': 'RIGHTMOUSE', 'value': 'PRESS', 'shift': True, 'ctrl': True} - ] - } - register_keymap_items = [] - - @classmethod - def get_keymap(cls, keymap_name): - k = bpy.context.window_manager.keyconfigs - addon = k.addon - a = k.active.keymaps.get(keymap_name, None) - return addon.keymaps.new( - keymap_name, - space_type=a.space_type, - region_type=a.region_type - ) - - @classmethod - def register(cls): - for keymap_name, keymap_items in cls.KEY_MAPS.items(): - km = cls.get_keymap(keymap_name) - for item in keymap_items: - - idname = item.get("idname") - key_type = item.get("type") - value = item.get("value") - - shift = item.get("shift", False) - ctrl = item.get("ctrl", False) - alt = item.get("alt", False) - - kmi = km.keymap_items.new(idname, key_type, value, shift=shift, ctrl=ctrl, alt=alt) - - if kmi: - properties = item.get("properties") - - if properties: - for name, value in properties: - setattr(kmi.properties, name, value) - cls.register_keymap_items.append((km, kmi)) - - @classmethod - def unregister(cls): - for keymap, kmi in cls.register_keymap_items: - try: - keymap.keymap_items.remove(kmi) - except ReferenceError as r: - print(r) - cls.register_keymap_items.clear() - - -def register(): - CustomKey.register() - - -def unregister(): - CustomKey.unregister() diff --git a/tool/development_key.py b/tool/development_key.py new file mode 100644 index 0000000..2b6465f --- /dev/null +++ b/tool/development_key.py @@ -0,0 +1,42 @@ +from ..keymap import get_keymap + +register_keymap_items = [] + + +def register(): + global register_keymap_items + + from ..registration import DEVELOPMENT_KEY_MAPS + + for keymap_name, keymap_items in DEVELOPMENT_KEY_MAPS.items(): + km = get_keymap(keymap_name) + for item in keymap_items: + idname = item.get("idname") + key_type = item.get("type") + value = item.get("value") + + shift = item.get("shift", False) + ctrl = item.get("ctrl", False) + alt = item.get("alt", False) + + kmi = km.keymap_items.new(idname, key_type, value, shift=shift, ctrl=ctrl, alt=alt) + + if kmi: + properties = item.get("properties") + if properties: + for name, value in properties.items(): + setattr(kmi.properties, name, value) + register_keymap_items.append((km, kmi)) + + +def unregister(): + global register_keymap_items + for keymap, kmi in register_keymap_items: + try: + keymap.keymap_items.remove(kmi) + except ReferenceError as e: + print(e.area) + import traceback + traceback.print_exc() + traceback.print_stack() + register_keymap_items.clear() diff --git a/tool/fast_open_addon_code.py b/tool/fast_open_addon_code.py deleted file mode 100644 index 9d7d6a1..0000000 --- a/tool/fast_open_addon_code.py +++ /dev/null @@ -1,286 +0,0 @@ -import os - -import addon_utils -import bpy -from bl_ui.space_userpref import USERPREF_PT_addons - -from ..public import PublicClass - - -class AddonDraw(bpy.types.USERPREF_PT_addons): - layout: bpy.types.UILayout - source_draw_func = None - - @classmethod - def register(cls): - source_func = bpy.types.USERPREF_PT_addons.draw - if source_func != cls.source_draw_func: - cls.source_draw_func = source_func - bpy.types.USERPREF_PT_addons.draw = AddonDraw.draw_addons - - @classmethod - def unregister(cls): - source_func = bpy.types.USERPREF_PT_addons.draw - if source_func == cls.draw_addons: - bpy.types.USERPREF_PT_addons.draw = cls.source_draw_func - - def draw_addons(self, context): - layout = self.layout - - wm = context.window_manager - prefs = context.preferences - used_ext = {ext.module for ext in prefs.addons} - - addon_user_dirs = PublicClass.get_addon_user_dirs() - # Development option for 2.8x, don't show users bundled addons - # unless they have been updated for 2.8x. - # Developers can turn them on with '--debug' - show_official_27x_addons = bpy.app.debug - - # collect the categories that can be filtered on - addons = [ - (mod, addon_utils.module_bl_info(mod)) - for mod in addon_utils.modules(refresh=False) - ] - - split = layout.split(factor=0.6) - - row = split.row() - row.prop(wm, "addon_support", expand=True) - - row = split.row(align=True) - row.operator("preferences.addon_install", - icon='IMPORT', text="Install...") - row.operator("preferences.addon_refresh", - icon='FILE_REFRESH', text="Refresh") - - row = layout.row() - row.prop(prefs.view, "show_addons_enabled_only") - row.prop(wm, "addon_filter", text="") - row.prop(wm, "addon_search", text="", icon='VIEWZOOM') - - col = layout.column() - - # set in addon_utils.modules_refresh() - if addon_utils.error_duplicates: - box = col.box() - row = box.row() - row.label(text="Multiple add-ons with the same name found!") - row.label(icon='ERROR') - box.label(text="Delete one of each pair to resolve:") - for (addon_name, addon_file, addon_path) in addon_utils.error_duplicates: - box.separator() - sub_col = box.column(align=True) - sub_col.label(text=addon_name + ":") - sub_col.label(text=" " + addon_file) - sub_col.label(text=" " + addon_path) - - if addon_utils.error_encoding: - self.draw_error( - col, - "One or more addons do not have UTF-8 encoding\n" - "(see console for details)", - ) - - show_enabled_only = prefs.view.show_addons_enabled_only - addon_filter = wm.addon_filter - search = wm.addon_search.lower() - support = wm.addon_support - - # initialized on demand - user_addon_paths = [] - - for mod, info in addons: - module_name = mod.__name__ - - is_enabled = module_name in used_ext - - if info["support"] not in support: - continue - - # check if addon should be visible with current filters - is_visible = ( - (addon_filter == "All") or - (addon_filter == info["category"]) or - (addon_filter == "User" and (mod.__file__.startswith(addon_user_dirs))) - ) - if show_enabled_only: - is_visible = is_visible and is_enabled - - if is_visible: - if search and not ( - (search in info["name"].lower()) or - (info["author"] and (search in info["author"].lower())) or - ((addon_filter == "All") and ( - search in info["category"].lower())) - ): - continue - - # Skip 2.7x add-ons included with Blender, unless in debug mode. - is_addon_27x = info.get("blender", (0,)) < (2, 80) - if ( - is_addon_27x and - (not show_official_27x_addons) and - (not mod.__file__.startswith(addon_user_dirs)) - ): - continue - - # Addon UI Code - col_box = col.column() - box = col_box.box() - colsub = box.column() - row = colsub.row(align=True) - - row.operator( - "preferences.addon_expand", - icon='DISCLOSURE_TRI_DOWN' if info["show_expanded"] else 'DISCLOSURE_TRI_RIGHT', - emboss=False, - ).module = module_name - - row.operator( - "preferences.addon_disable" if is_enabled else "preferences.addon_enable", - icon='CHECKBOX_HLT' if is_enabled else 'CHECKBOX_DEHLT', text="", - emboss=False, - ).module = module_name - - sub = row.row() - sub.active = is_enabled - sub.label(text="%s: %s" % (info["category"], info["name"])) - - # use disabled state for old add-ons, chances are they are broken. - if is_addon_27x: - sub.label(text="Upgrade to 2.8x required") - sub.label(icon='ERROR') - # Remove code above after 2.8x migration is complete. - elif info["warning"]: - sub.label(icon='ERROR') - - # icon showing support level. - sub.label(icon=self._support_icon_mapping.get( - info["support"], 'QUESTION')) - - # Expanded UI (only if additional info is available) - if info["show_expanded"]: - if info["description"]: - split = colsub.row().split(factor=0.15) - split.label(text="Description:") - split.label(text=info["description"]) - if info["location"]: - split = colsub.row().split(factor=0.15) - split.label(text="Location:") - split.label(text=info["location"]) - if mod: - split = colsub.row().split(factor=0.15) - split.label(text="File:") - split.label(text=mod.__file__, translate=False) - if info["author"]: - split = colsub.row().split(factor=0.15) - split.label(text="Author:") - split.label(text=info["author"], translate=False) - if info["version"]: - split = colsub.row().split(factor=0.15) - split.label(text="Version:") - split.label(text=".".join(str(x) - for x in info["version"]), translate=False) - if info["warning"]: - split = colsub.row().split(factor=0.15) - split.label(text="Warning:") - split.label(text=" " + info["warning"], icon='ERROR') - - user_addon = USERPREF_PT_addons.is_user_addon(mod, user_addon_paths) - tot_row = bool(info["doc_url"]) + bool(user_addon) - - if tot_row: - split = colsub.row().split(factor=0.15) - split.label(text="Internet:") - sub = split.row() - if info["doc_url"]: - sub.operator( - "wm.url_open", text="Documentation", icon='HELP', - ).url = info["doc_url"] - # Only add "Report a Bug" button if tracker_url is set - # or the add-on is bundled (use official tracker then). - if info.get("tracker_url"): - sub.operator( - "wm.url_open", text="Report a Bug", icon='URL', - ).url = info["tracker_url"] - elif not user_addon: - addon_info = ( - "Name: %s %s\n" - "Author: %s\n" - ) % (info["name"], str(info["version"]), info["author"]) - props = sub.operator( - "wm.url_open_preset", text="Report a Bug", icon='URL', - ) - props.type = 'BUG_ADDON' - props.id = addon_info - - AddonDraw.draw_addon_add_item(sub, user_addon, mod) - # Show addon user preferences - if is_enabled: - addon_preferences = prefs.addons[module_name].preferences - if addon_preferences is not None: - draw = getattr(addon_preferences, "draw", None) - if draw is not None: - addon_preferences_class = type( - addon_preferences) - box_prefs = col_box.box() - box_prefs.label(text="Preferences:") - addon_preferences_class.layout = box_prefs - try: - draw(context) - except: - import traceback - traceback.print_exc() - box_prefs.label( - text="Error (see console)", icon='ERROR') - del addon_preferences_class.layout - - # Append missing scripts - # First collect scripts that are used but have no script file. - module_names = {mod.__name__ for mod, info in addons} - missing_modules = {ext for ext in used_ext if ext not in module_names} - - if missing_modules and addon_filter in {"All", "Enabled"}: - col.column().separator() - col.column().label(text="Missing script files") - - module_names = {mod.__name__ for mod, info in addons} - for module_name in sorted(missing_modules): - is_enabled = module_name in used_ext - # Addon UI Code - box = col.column().box() - colsub = box.column() - row = colsub.row(align=True) - - row.label(text="", icon='ERROR') - - if is_enabled: - row.operator( - "preferences.addon_disable", icon='CHECKBOX_HLT', text="", emboss=False, - ).module = module_name - - row.label(text=module_name, translate=False) - - @staticmethod - def draw_addon_add_item(layout, user_addon, mod): - row = layout.row(align=True) - # 添加打开文件夹按钮 - if user_addon: - row.operator( - "preferences.addon_remove", text="Remove", icon='CANCEL', - ).module = mod.__name__ - row.alert = True - row.operator('wm.path_open', - text='Open Script').filepath = mod.__file__ - folder_path, file_path = os.path.split(mod.__file__) - row.operator('wm.path_open', icon='FILEBROWSER', - text='').filepath = folder_path - - -def register(): - AddonDraw.register() - - -def unregister(): - AddonDraw.unregister() diff --git a/tool/open_addon_script.py b/tool/open_addon_script.py new file mode 100644 index 0000000..ff98ce3 --- /dev/null +++ b/tool/open_addon_script.py @@ -0,0 +1,399 @@ +import os + +"""TODO +This feature seems to be unused and will not be used temporarily +And as Blender iterates, unpredictable errors may occur +""" + +import addon_utils +import bpy +from bl_ui.space_userpref import USERPREF_PT_addons + +from ..utils import get_addon_user_dirs + + +def draw_addon_add_item(layout, user_addon, mod): + row = layout.row(align=True) + # 添加打开文件夹按钮 + if user_addon: + row.operator( + "preferences.addon_remove", text="Remove", icon='CANCEL', + ).module = mod.__name__ + row.alert = True + row.operator('wm.path_open', + text='Open Script').filepath = mod.__file__ + folder_path, file_path = os.path.split(mod.__file__) + row.operator('wm.path_open', icon='FILEBROWSER', + text='').filepath = folder_path + + +def draw_addon_4_1_or_below(self, context): + layout = self.layout + + wm = context.window_manager + prefs = context.preferences + used_ext = {ext.module for ext in prefs.addons} + + addon_user_dirs = get_addon_user_dirs() + # Development option for 2.8x, don't show users bundled addons + # unless they have been updated for 2.8x. + # Developers can turn them on with '--debug' + show_official_27x_addons = bpy.app.debug + + # collect the categories that can be filtered on + addons = [ + (mod, addon_utils.module_bl_info(mod)) + for mod in addon_utils.modules(refresh=False) + ] + + split = layout.split(factor=0.6) + + row = split.row() + row.prop(wm, "addon_support", expand=True) + + row = split.row(align=True) + row.operator("preferences.addon_install", + icon='IMPORT', text="Install...") + row.operator("preferences.addon_refresh", + icon='FILE_REFRESH', text="Refresh") + + row = layout.row() + row.prop(prefs.view, "show_addons_enabled_only") + row.prop(wm, "addon_filter", text="") + row.prop(wm, "addon_search", text="", icon='VIEWZOOM') + + col = layout.column() + + # set in addon_utils.modules_refresh() + if addon_utils.error_duplicates: + box = col.box() + row = box.row() + row.label(text="Multiple add-ons with the same name found!") + row.label(icon='ERROR') + box.label(text="Delete one of each pair to resolve:") + for (addon_name, addon_file, addon_path) in addon_utils.error_duplicates: + box.separator() + sub_col = box.column(align=True) + sub_col.label(text=addon_name + ":") + sub_col.label(text=" " + addon_file) + sub_col.label(text=" " + addon_path) + + if addon_utils.error_encoding: + self.draw_error( + col, + "One or more addons do not have UTF-8 encoding\n" + "(see console for details)", + ) + + show_enabled_only = prefs.view.show_addons_enabled_only + addon_filter = wm.addon_filter + search = wm.addon_search.lower() + support = wm.addon_support + + # initialized on demand + user_addon_paths = [] + + for mod, info in addons: + module_name = mod.__name__ + + is_enabled = module_name in used_ext + + if info["support"] not in support: + continue + + # check if addon should be visible with current filters + is_visible = ( + (addon_filter == "All") or + (addon_filter == info["category"]) or + (addon_filter == "User" and (mod.__file__.startswith(addon_user_dirs))) + ) + if show_enabled_only: + is_visible = is_visible and is_enabled + + if is_visible: + if search and not ( + (search in info["name"].lower()) or + (info["author"] and (search in info["author"].lower())) or + ((addon_filter == "All") and ( + search in info["category"].lower())) + ): + continue + + # Skip 2.7x add-ons included with Blender, unless in debug mode. + is_addon_27x = info.get("blender", (0,)) < (2, 80) + if ( + is_addon_27x and + (not show_official_27x_addons) and + (not mod.__file__.startswith(addon_user_dirs)) + ): + continue + + # Addon UI Code + col_box = col.column() + box = col_box.box() + colsub = box.column() + row = colsub.row(align=True) + + row.operator( + "preferences.addon_expand", + icon='DISCLOSURE_TRI_DOWN' if info["show_expanded"] else 'DISCLOSURE_TRI_RIGHT', + emboss=False, + ).module = module_name + + row.operator( + "preferences.addon_disable" if is_enabled else "preferences.addon_enable", + icon='CHECKBOX_HLT' if is_enabled else 'CHECKBOX_DEHLT', text="", + emboss=False, + ).module = module_name + + sub = row.row() + sub.active = is_enabled + sub.label(text="%s: %s" % (info["category"], info["name"])) + + # use disabled state for old add-ons, chances are they are broken. + if is_addon_27x: + sub.label(text="Upgrade to 2.8x required") + sub.label(icon='ERROR') + # Remove code above after 2.8x migration is complete. + elif info["warning"]: + sub.label(icon='ERROR') + + # icon showing support level. + sub.label(icon=self._support_icon_mapping.get( + info["support"], 'QUESTION')) + + # Expanded UI (only if additional info is available) + if info["show_expanded"]: + if info["description"]: + split = colsub.row().split(factor=0.15) + split.label(text="Description:") + split.label(text=info["description"]) + if info["location"]: + split = colsub.row().split(factor=0.15) + split.label(text="Location:") + split.label(text=info["location"]) + if mod: + split = colsub.row().split(factor=0.15) + split.label(text="File:") + split.label(text=mod.__file__, translate=False) + if info["author"]: + split = colsub.row().split(factor=0.15) + split.label(text="Author:") + split.label(text=info["author"], translate=False) + if info["version"]: + split = colsub.row().split(factor=0.15) + split.label(text="Version:") + split.label(text=".".join(str(x) + for x in info["version"]), translate=False) + if info["warning"]: + split = colsub.row().split(factor=0.15) + split.label(text="Warning:") + split.label(text=" " + info["warning"], icon='ERROR') + + user_addon = USERPREF_PT_addons.is_user_addon(mod, user_addon_paths) + tot_row = bool(info["doc_url"]) + bool(user_addon) + + if tot_row: + split = colsub.row().split(factor=0.15) + split.label(text="Internet:") + sub = split.row() + if info["doc_url"]: + sub.operator( + "wm.url_open", text="Documentation", icon='HELP', + ).url = info["doc_url"] + # Only add "Report a Bug" button if tracker_url is set + # or the add-on is bundled (use official tracker then). + if info.get("tracker_url"): + sub.operator( + "wm.url_open", text="Report a Bug", icon='URL', + ).url = info["tracker_url"] + elif not user_addon: + addon_info = ( + "Name: %s %s\n" + "Author: %s\n" + ) % (info["name"], str(info["version"]), info["author"]) + props = sub.operator( + "wm.url_open_preset", text="Report a Bug", icon='URL', + ) + props.type = 'BUG_ADDON' + props.id = addon_info + + draw_addon_add_item(sub, user_addon, mod) + # Show addon user preferences + if is_enabled: + addon_preferences = prefs.addons[module_name].preferences + if addon_preferences is not None: + draw = getattr(addon_preferences, "draw", None) + if draw is not None: + addon_preferences_class = type( + addon_preferences) + box_prefs = col_box.box() + box_prefs.label(text="Preferences:") + addon_preferences_class.layout = box_prefs + try: + draw(context) + except: + import traceback + traceback.print_exc() + box_prefs.label( + text="Error (see console)", icon='ERROR') + del addon_preferences_class.layout + + # Append missing scripts + # First collect scripts that are used but have no script file. + module_names = {mod.__name__ for mod, info in addons} + missing_modules = {ext for ext in used_ext if ext not in module_names} + + if missing_modules and addon_filter in {"All", "Enabled"}: + col.column().separator() + col.column().label(text="Missing script files") + + module_names = {mod.__name__ for mod, info in addons} + for module_name in sorted(missing_modules): + is_enabled = module_name in used_ext + # Addon UI Code + box = col.column().box() + colsub = box.column() + row = colsub.row(align=True) + + row.label(text="", icon='ERROR') + + if is_enabled: + row.operator( + "preferences.addon_disable", icon='CHECKBOX_HLT', text="", emboss=False, + ).module = module_name + + row.label(text=module_name, translate=False) + + +# scripts/addons_core/bl_pkg/bl_extension_ui.py l:212 +def draw_addon_4_2_or_above( + *, + layout, # `bpy.types.UILayout` + mod, # `ModuleType` + addon_type, # `int` + is_enabled, # `bool` + # Expanded from both legacy add-ons & extensions. + # item_name, # `str` # UNUSED. + item_description, # `str` + item_maintainer, # `str` + item_version, # `str` + item_warnings, # `list[str]` + item_doc_url, # `str` + item_tracker_url, # `str` +): + from bl_pkg.bl_extension_ui import ( + ADDON_TYPE_LEGACY_USER, ADDON_TYPE_LEGACY_CORE, ADDON_TYPE_LEGACY_OTHER, ADDON_TYPE_EXTENSION, + USE_SHOW_ADDON_TYPE_AS_TEXT,addon_type_name, + ) + + from bpy.app.translations import ( + contexts as i18n_contexts, + ) + + split = layout.split(factor=0.8) + col_a = split.column() + col_b = split.column() + + if item_description: + col_a.label( + text=" {:s}.".format(item_description), + translate=False, + ) + + rowsub = col_b.row() + rowsub.alignment = 'RIGHT' + if addon_type == ADDON_TYPE_LEGACY_CORE: + rowsub.active = False + rowsub.label(text="Built-in") + rowsub.separator() + elif addon_type == ADDON_TYPE_LEGACY_USER: + rowsub.operator("preferences.addon_remove", text="Uninstall").module = mod.__name__ + del rowsub + + layout.separator(type='LINE') + + sub = layout.column() + sub.active = is_enabled + split = sub.split(factor=0.15) + col_a = split.column() + col_b = split.column() + + col_a.alignment = 'RIGHT' + + if item_doc_url: + col_a.label(text="Website") + col_b.split(factor=0.5).operator( + "wm.url_open", + text=domain_extract_from_url(item_doc_url), + icon='HELP' if addon_type in {ADDON_TYPE_LEGACY_CORE, ADDON_TYPE_LEGACY_USER} else 'URL', + ).url = item_doc_url + # Only add "Report a Bug" button if tracker_url is set. + # None of the core add-ons are expected to have tracker info (glTF is the exception). + if item_tracker_url: + col_a.label(text="Feedback", text_ctxt=i18n_contexts.editor_preferences) + col_b.split(factor=0.5).operator( + "wm.url_open", text="Report a Bug", icon='URL', + ).url = item_tracker_url + + if USE_SHOW_ADDON_TYPE_AS_TEXT: + col_a.label(text="Type") + col_b.label(text=addon_type_name[addon_type]) + if item_maintainer: + col_a.label(text="Maintainer") + col_b.label(text=item_maintainer, translate=False) + if item_version: + col_a.label(text="Version") + col_b.label(text=item_version, translate=False) + if item_warnings: + # Only for legacy add-ons. + col_a.label(text="Warning") + col_b.label(text=item_warnings[0], icon='ERROR') + if len(item_warnings) > 1: + for value in item_warnings[1:]: + col_a.label(text="") + col_b.label(text=value, icon='BLANK1') + # pylint: disable-next=undefined-loop-variable + del value + + if addon_type != ADDON_TYPE_LEGACY_CORE: + col_a.label(text="File") + col_b.label(text=mod.__file__, translate=False) + + draw_addon_add_item(col_b, False, mod) + + +is_4_2_or_above = bpy.app.version[:2] >= (4, 2) +source_draw_func = None + + +def register(): + global source_draw_func + + if is_4_2_or_above: + import bl_pkg + source_draw_func = bl_pkg.bl_extension_ui.addon_draw_item_expanded + bl_pkg.bl_extension_ui.addon_draw_item_expanded = draw_addon_4_2_or_above + else: + source_func = bpy.types.USERPREF_PT_addons.draw + if source_func != draw_addon_4_1_or_below: + source_draw_func = source_func + + if source_draw_func is None: + source_draw_func = source_func + + bpy.types.USERPREF_PT_addons.draw = draw_addon_4_1_or_below + + +def unregister(): + global source_draw_func + + if is_4_2_or_above: + import bl_pkg + if bl_pkg.bl_extension_ui.addon_draw_item_expanded == draw_addon_4_2_or_above: + bl_pkg.bl_extension_ui.addon_draw_item_expanded = source_draw_func + else: + source_func = bpy.types.USERPREF_PT_addons.draw + if source_func == draw_addon_4_1_or_below: + bpy.types.USERPREF_PT_addons.draw = source_draw_func + source_draw_func = None diff --git a/tool/remember_addon_expanded.py b/tool/remember_addon_expanded.py new file mode 100644 index 0000000..9309c56 --- /dev/null +++ b/tool/remember_addon_expanded.py @@ -0,0 +1,48 @@ +import bpy + +from ..utils import get_pref, addon_keys + + +def remember_addons_expanded(): + pref = get_pref() + + import addon_utils + addon_utils.modules_refresh() + + addon_show_expanded = pref.addon_show_expanded + + for key, mod in addon_utils.modules().mapping.items(): + info = addon_utils.module_bl_info(mod) + show_expanded = info.get("show_expanded", False) + + item = addon_show_expanded.get(key, None) + if item is None: + item = addon_show_expanded.add() + item.name = key + item.show_expanded = show_expanded + + bpy.ops.wm.save_userpref() + + +def restore_addons_expanded(): + def restore(): + pref = get_pref() + + if pref.activate_remember_addon_expanded: + expanded_list = [addon.name for addon in pref.addon_show_expanded if addon.show_expanded] + + import addon_utils + addon_utils.modules_refresh() + for key in addon_keys(): + if key in expanded_list: + bpy.ops.preferences.addon_expand(module=key) + + bpy.app.timers.register(restore, first_interval=1, persistent=True) + + +def register(): + ... + + +def unregister(): + remember_addons_expanded() diff --git a/tool/remember_addon_search.py b/tool/remember_addon_search.py new file mode 100644 index 0000000..a7f83e9 --- /dev/null +++ b/tool/remember_addon_search.py @@ -0,0 +1,49 @@ +import bpy +from bpy.app.handlers import persistent + +from ..utils import get_pref, tag_redraw + +owner = object() + + +def remember_addon_search(): + """if bpy.context.window_manager.addon_search change then call remember""" + get_pref().addon_search = bpy.context.window_manager.addon_search + + +def restore_addon_search(): + """delayed set addon search property""" + pref = get_pref() + + def restore(): + bpy.context.window_manager.addon_search = pref.addon_search + tag_redraw() + + if pref.activate_remember_addon_search: + bpy.app.timers.register(restore, first_interval=1, persistent=True) + + +def addon_search_msgbus(): + """msgbus wm addon search""" + bpy.msgbus.subscribe_rna( + key=(bpy.types.WindowManager, "addon_search"), + owner=owner, + args=(), + notify=remember_addon_search, + ) + + +@persistent +def load_post_handler(self, context): + """if load file reregister msgbus 避免丢失监听""" + addon_search_msgbus() + + +def register(): + addon_search_msgbus() + + bpy.app.handlers.load_post.append(load_post_handler) + + +def unregister(): + bpy.msgbus.clear_by_owner(owner) diff --git a/tool/restart_blender.py b/tool/restart_blender.py index 74d7c7d..fe66f50 100644 --- a/tool/restart_blender.py +++ b/tool/restart_blender.py @@ -1,37 +1,47 @@ import platform import bpy -from bpy.props import IntProperty -from bpy.types import Operator -from ..public import PublicClass +from ..utils import PublicEvent def start_blender(): + """Create a new Blender thread through subprocess""" import subprocess bpy.ops.wm.save_userpref() subprocess.Popen([bpy.app.binary_path]) -class RestartBlender(Operator, - PublicClass): - bl_idname = 'wm.restart_blender' - bl_label = 'Restart Blender' - bl_description = ''' - Left - Open a New Blender - - alt+Left -Prompt to save file, Restart blender - ctrl+Left - Do not prompt to save files, Restart Blender - shift+Left - Open Tow Blender - - ctrl+alt+shift+Left Loop Open Blender, dedicated for explosion''' - bl_options = {'REGISTER'} - - open_blender_number: IntProperty(name='Open Blender Number', - default=20, - max=114514, - min=3, - subtype='FACTOR') +class RestartBlender( + bpy.types.Operator, + PublicEvent, +): + bl_idname = "wm.restart_blender" + bl_label = "Restart Blender" + bl_description = """ + """ + bl_options = {"REGISTER"} + + @classmethod + def description(cls, context, properties): + from ..translation import translate_lines_text + return translate_lines_text( + "", + "Click Open a New Blender", + "Alt Prompt to save file, Restart Blender", + "Ctrl Do not prompt to save files, Restart Blender", + "Shift Open Tow Blender", + "", + "Ctrl+Alt+Shift Loop Open Blender, dedicated for explosion", + ) + + open_blender_number: bpy.props.IntProperty( + name="Open Blender Number", + default=20, + max=114514, + min=3, + subtype="FACTOR" + ) @staticmethod def for_open(num): @@ -43,7 +53,6 @@ def run_cmd(self, event: bpy.types.Event): self.set_event_key(event) start_blender() if self.not_key: - # bpy.ops.wm.window_close() ... elif self.only_alt: bpy.ops.wm.window_close() @@ -57,40 +66,37 @@ def run_cmd(self, event: bpy.types.Event): elif self.ctrl_shift_alt: self.for_open(self.open_blender_number) else: - self.report({'INFO'}, self.bl_description) + self.report({"INFO"}, self.bl_description) def invoke(self, context, event): - if platform.system() == 'Windows': + if platform.system() == "Windows": self.run_cmd(event) - elif platform.system() == 'Linux': - print('This feature currently does not support Linux systems') + elif platform.system() == "Linux": + self.report({"INFO"}, "This feature currently does not support Linux systems") else: - print('This feature currently does not support this system') - return {'FINISHED'} - - def draw_top_bar(self, context): - layout = self.layout - row = layout.row(align=True) - a = row.row() - a.alert = True - a.operator(operator=RestartBlender.bl_idname, - text="", emboss=False, icon='QUIT') - - -register_class, unregister_class = bpy.utils.register_classes_factory( - ( - RestartBlender, + self.report({"INFO"}, "This feature currently does not support this system") + return {"FINISHED"} + + +def draw_restart_blender_top_bar(self, context): + row = self.layout.row(align=True) + row.alert = True + row.operator( + operator=RestartBlender.bl_idname, + text="", + emboss=False, + icon="QUIT" ) -) def register(): - register_class() - if hasattr(bpy.types, 'TOPBAR_MT_editor_menus'): - bpy.types.TOPBAR_MT_editor_menus.append(RestartBlender.draw_top_bar) # 顶部标题栏 + bpy.utils.register_class(RestartBlender) + if hasattr(bpy.types, "TOPBAR_MT_editor_menus"): + bpy.types.TOPBAR_MT_editor_menus.append(draw_restart_blender_top_bar) def unregister(): - unregister_class() - if hasattr(bpy.types, 'TOPBAR_MT_editor_menus'): - bpy.types.TOPBAR_MT_editor_menus.remove(RestartBlender.draw_top_bar) + if getattr(RestartBlender, "is_registered", False): + bpy.utils.unregister_class(RestartBlender) + if hasattr(bpy.types, "TOPBAR_MT_editor_menus"): + bpy.types.TOPBAR_MT_editor_menus.remove(draw_restart_blender_top_bar) diff --git a/translation.py b/translation.py new file mode 100644 index 0000000..1923b64 --- /dev/null +++ b/translation.py @@ -0,0 +1,92 @@ +import ast +import re + +import bpy + +zh_HANS = { + "Automatically reload scripts and run them": "自动重载脚本并运行", + "ReLoad Script": "重载脚本", + "Development Keymap": "开发快捷键", + "Commonly used Keymaps to speed up the development process": "常用可以加快开发流程的快捷键", + "Addon Open": "打开插件", + "Rewrite the drawing method of the addon section, and display it in the expansion of the addon": "重写插件部分的绘制方法,并在插件的扩展是显示打开脚本文件与文件夹按钮", + "Remember addon expanded": "记住插件展开", + "Record the expanded Addon and restore it the next time you open Blender": "将已展开的插件记录下来,在下次打开Blender时恢复", + "Remember addon search": "记住插件搜索", + "Record the Addon search and restore it the next time you start Blender": "将插件搜索记录下来,在下次启动Blender时恢复", + "Restart Blender": "重启Blender", + "Enable multiple Blenders or restart Blender, please be careful to save the edit file!!!!": "启用多个Blender或重启Blender,请注意保存编辑文件!!!!", + "Keymap": "快捷键", + "Click Open a New Blender": "左键 打开一个新的Blender", + "Alt Prompt to save file, Restart Blender": "Alt 提示保存文件,重启Blender", + "Ctrl Do not prompt to save files, Restart Blender": "Ctrl 不提示保存文件,重启Blender", + "Shift Open Tow Blender": "Shift 打开两个Blender", + "Ctrl+Alt+Shift Loop Open Blender, dedicated for explosion": "Ctrl+Alt+Shift 循环打开Blender,爆炸专用", +} + + +def translate_lines_text(*args, split="\n"): + from bpy.app.translations import pgettext_iface + return split.join([pgettext_iface(line) for line in args]) + + +def get_language_list() -> list: + """ + Traceback (most recent call last): + File "", line 1, in +TypeError: bpy_struct: item.attr = val: enum "a" not found in ("DEFAULT", "en_US", "es", "ja_JP", "sk_SK", "vi_VN", "zh_HANS", "ar_EG", "de_DE", "fr_FR", "it_IT", "ko_KR", "pt_BR", "pt_PT", "ru_RU", "uk_UA", "zh_TW", "ab", "ca_AD", "cs_CZ", "eo", "eu_EU", "fa_IR", "ha", "he_IL", "hi_IN", "hr_HR", "hu_HU", "id_ID", "ky_KG", "nl_NL", "pl_PL", "sr_RS", "sr_RS@latin", "sv_SE", "th_TH", "tr_TR") + """ + try: + bpy.context.preferences.view.language = "" + except TypeError as e: + matches = re.findall(r"\(([^()]*)\)", e.args[-1]) + return ast.literal_eval(f"({matches[-1]})") + + +class TranslationHelper: + def __init__(self, name: str, data: dict, lang='zh_CN'): + self.name = name + self.translations_dict = dict() + + for src, src_trans in data.items(): + key = ("Operator", src) + self.translations_dict.setdefault(lang, {})[key] = src_trans + key = ("*", src) + self.translations_dict.setdefault(lang, {})[key] = src_trans + key = (name, src) + self.translations_dict.setdefault(lang, {})[key] = src_trans + + def register(self): + try: + bpy.app.translations.register(self.name, self.translations_dict) + except(ValueError): + pass + + def unregister(self): + bpy.app.translations.unregister(self.name) + + +# Set +############ + +all_language = get_language_list() + +zh_CN = None + + +def register(): + global zh_CN + + language = "zh_CN" + if language not in all_language: + if language in ("zh_CN", "zh_HANS"): + if "zh_CN" in all_language: + language = "zh_CN" + elif "zh_HANS" in all_language: + language = "zh_HANS" + zh_CN = TranslationHelper('development_kit_zh_CN', zh_HANS, lang=language) + zh_CN.register() + + +def unregister(): + zh_CN.unregister() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..d0b7c2d --- /dev/null +++ b/utils.py @@ -0,0 +1,90 @@ +import os +from functools import cache + +import bpy + + +@cache +def get_pref(): + return bpy.context.preferences.addons[__package__].preferences + + +def get_event_key(event: bpy.types.Event): + alt = event.alt + shift = event.shift + ctrl = event.ctrl + + not_key = ((not ctrl) and (not alt) and (not shift)) + + only_ctrl = (ctrl and (not alt) and (not shift)) + only_alt = ((not ctrl) and alt and (not shift)) + only_shift = ((not ctrl) and (not alt) and shift) + + shift_alt = ((not ctrl) and alt and shift) + ctrl_alt = (ctrl and alt and (not shift)) + + ctrl_shift = (ctrl and (not alt) and shift) + ctrl_shift_alt = (ctrl and alt and shift) + return not_key, only_ctrl, only_alt, only_shift, shift_alt, ctrl_alt, ctrl_shift, ctrl_shift_alt + + +def tag_redraw(): + for area in bpy.context.screen.areas: + area.tag_redraw() + + +def addon_keys() -> "[str]": + """获取插件所有id""" + import addon_utils + if bpy.app.version >= (4, 2, 0): + return addon_utils.modules().mapping.keys() + else: + return [addon.__name__ for addon in addon_utils.modules()] + + +def clear_cache(): + get_pref.cache_clear() + + +def get_addon_user_dirs(): + version = bpy.app.version + if version >= (3, 6, 0): # 4.0以上 + addon_user_dirs = tuple( + i for i in ( + *[os.path.join(pref_p, "addons") for pref_p in bpy.utils.script_paths_pref()], + bpy.utils.user_resource('SCRIPTS', path="addons"), + ) + if i + ) + elif bpy.app.version >= (2, 94, 0): # 3.0 version + addon_user_dirs = tuple( + i for i in ( + os.path.join(bpy.context.preferences.filepaths.script_directory, "addons"), + bpy.utils.user_resource('SCRIPTS', path="addons"), + ) + if i + ) + else: # 2.93 version + addon_user_dirs = tuple( + i for i in ( + os.path.join(bpy.context.preferences.filepaths.script_directory, "addons"), + bpy.utils.user_resource('SCRIPTS', "addons"), + ) + if i + ) + return addon_user_dirs + + +class PublicEvent: + not_key: bool + only_ctrl: bool + only_alt: bool + only_shift: bool + shift_alt: bool + ctrl_alt: bool + ctrl_shift: bool + ctrl_shift_alt: bool + + def set_event_key(self, event): + self.not_key, self.only_ctrl, self.only_alt, self.only_shift, self.shift_alt, self.ctrl_alt, self.ctrl_shift, self.ctrl_shift_alt = \ + get_event_key(event)