From 72ef03d5a192e70eb42f788fb4bc5d07f2f160fa Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Fri, 21 Jun 2024 10:42:53 +1000 Subject: [PATCH] Extensions: split add-ons & extensions into separate preferences Add back the "Add-ons" preferences, removing add-on logic from extensions. - Add support for filtering add-ons by tags (separate from extension tags). - Tags now respect the "Only Enabled" option. - Remove the ability to enable/disable add-ons from extensions. - Remove add-on preferences from extensions. - Remove "Legacy" & "Core" prefix from add-on names. - Remove "Show Legacy Add-ons" filtering option. Implements design task #122735. Details: - Add-on names and descriptions are no longer translated, since it's impractical to translate text which is mostly maintained outside of Blender. - Extensions names have a `[disabled]` suffix when disabled so it's possible to identify installed but disabled extensions. - The add-on "type" is shown in the details, so it's possible to tell the difference between an extension, a core add-on & a legacy user add-on. - Icons are also used to differentiate the add-on type. - User add-on's must be uninstalled from the add-ons section (matching 4.1 behavior). - Simplify logic for filtering tags, move into a function. --- scripts/addons_core/bl_pkg/__init__.py | 10 +- scripts/addons_core/bl_pkg/bl_extension_ui.py | 1189 +++++++++++------ scripts/startup/bl_operators/userpref.py | 2 +- scripts/startup/bl_ui/space_userpref.py | 20 +- source/blender/makesdna/DNA_userdef_types.h | 3 +- source/blender/makesrna/intern/rna_userdef.cc | 1 + 6 files changed, 783 insertions(+), 442 deletions(-) diff --git a/scripts/addons_core/bl_pkg/__init__.py b/scripts/addons_core/bl_pkg/__init__.py index 42d8b539ec8..66700f87421 100644 --- a/scripts/addons_core/bl_pkg/__init__.py +++ b/scripts/addons_core/bl_pkg/__init__.py @@ -552,6 +552,10 @@ def register(): bl_extension_ops.register() bl_extension_ui.register() + WindowManager.addon_tags = PointerProperty( + name="Addon Tags", + type=BlExtDummyGroup, + ) WindowManager.extension_tags = PointerProperty( name="Extension Tags", type=BlExtDummyGroup, @@ -583,11 +587,6 @@ def register(): name="Show Installed Extensions", description="Only show installed extensions", ) - WindowManager.extension_show_legacy_addons = BoolProperty( - name="Show Legacy Add-ons", - description="Show add-ons which are not packaged as extensions", - default=True, - ) from bl_ui.space_userpref import USERPREF_MT_interface_theme_presets USERPREF_MT_interface_theme_presets.append(theme_preset_draw) @@ -623,7 +622,6 @@ def unregister(): del WindowManager.extension_type del WindowManager.extension_enabled_only del WindowManager.extension_installed_only - del WindowManager.extension_show_legacy_addons for cls in classes: bpy.utils.unregister_class(cls) diff --git a/scripts/addons_core/bl_pkg/bl_extension_ui.py b/scripts/addons_core/bl_pkg/bl_extension_ui.py index bda3859b5e0..d36981e8ed7 100644 --- a/scripts/addons_core/bl_pkg/bl_extension_ui.py +++ b/scripts/addons_core/bl_pkg/bl_extension_ui.py @@ -22,13 +22,24 @@ from bpy.types import ( from bl_ui.space_userpref import ( USERPREF_PT_addons, + USERPREF_PT_extensions, USERPREF_MT_extensions_active_repo, ) +# We may want to change these. +USE_EXTENSIONS_CHECKBOX = False +USE_EXTENSIONS_ADDON_PREFS = False +# TODO: choose how to show this when an add-on is an extension/core/legacy. +# The information is somewhat useful as you can only remove legacy add-ons from the add-ons sections. +# Whereas extensions must be removed from the extensions section. +# So without showing a distinction - the existance of these buttons is not clear. +USE_SHOW_ADDON_TYPE_AS_TEXT = True +USE_SHOW_ADDON_TYPE_AS_ICON = True + + # ----------------------------------------------------------------------------- # Generic Utilities - def size_as_fmt_string(num: float, *, precision: int = 1) -> str: for unit in ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"): if abs(num) < 1024.0: @@ -48,6 +59,9 @@ def sizes_as_percentage_string(size_partial: int, size_final: int) -> str: return "{:-6.2f}%".format(percent * 100) +# ----------------------------------------------------------------------------- +# Shared Utilities (Extensions / Legacy Add-ons) + def pkg_repo_and_id_from_theme_path(repos_all, filepath): import os if not filepath: @@ -63,6 +77,10 @@ def pkg_repo_and_id_from_theme_path(repos_all, filepath): return None +def pkg_repo_module_prefix(repo): + return "bl_ext.{:s}.".format(repo.module) + + def module_parent_dirname(module_filepath): """ Return the name of the directory above the module (it's name only). @@ -88,27 +106,275 @@ def domain_extract_from_url(url): return domain +def pkg_manifest_zip_all_items(pkg_manifest_local, pkg_manifest_remote): + if pkg_manifest_remote is None: + if pkg_manifest_local is not None: + for pkg_id, item_local in pkg_manifest_local.items(): + yield pkg_id, (item_local, None) + # If both are none, there are no items, that's OK. + return + elif pkg_manifest_local is None: + for pkg_id, item_remote in pkg_manifest_remote.items(): + yield pkg_id, (None, item_remote) + return + + assert (pkg_manifest_remote is not None) and (pkg_manifest_local is not None) + pkg_manifest_local_copy = pkg_manifest_local.copy() + for pkg_id, item_remote in pkg_manifest_remote.items(): + yield pkg_id, (pkg_manifest_local_copy.pop(pkg_id, None), item_remote) + # Orphan packages (if they exist). + for pkg_id, item_local in pkg_manifest_local_copy.items(): + yield pkg_id, (item_local, None) + + # ----------------------------------------------------------------------------- -# Extensions UI (Legacy) +# Add-ons UI + +# While this is not a strict definition (internally they're just add-ons from different places), +# for the purposes of the UI it makes sense to differentiate add-ons this way because these add-on +# characteristics are mutually exclusive (there is no such thing as a user-core-extension for e.g.). + +# Add-On Types: + +# Any add-on which is an extension. +ADDON_TYPE_EXTENSION = 0 +# Any add-on bundled with Blender (in the `addons_core` directory). +# These cannot be removed. +ADDON_TYPE_LEGACY_CORE = 1 +# Any add-on from a user directory (add-ons from the users home directory or additional scripts directories). +ADDON_TYPE_LEGACY_USER = 2 +# Any add-on which does not match any of the above characteristics. +# This is most likely from `os.path.join(bpy.utils.resource_path('LOCAL'), "scripts", "addons")`. +# In this context, the difference between this an any other add-on is not important, +# so there is no need to go to the effort of differentiating `LOCAL` from other kinds of add-ons. +# If instances of "Legacy (Other)" add-ons exist in a default installation, this may be an error, +# otherwise, it's not a problem if these occur with customized user-configurations. +ADDON_TYPE_LEGACY_OTHER = 3 + +addon_type_name = ( + "Extension", # `ADDON_TYPE_EXTENSION`. + "Core", # `ADDON_TYPE_LEGACY_CORE`. + "Legacy (User)", # `ADDON_TYPE_LEGACY_USER`. + "Legacy (Other)", # `ADDON_TYPE_LEGACY_OTHER`. +) + +addon_type_icon = ( + 'COMMUNITY', # `ADDON_TYPE_EXTENSION`. + 'BLENDER', # `ADDON_TYPE_LEGACY_CORE`. + 'FILE_FOLDER', # `ADDON_TYPE_LEGACY_USER`. + 'PACKAGE', # `ADDON_TYPE_LEGACY_OTHER`. +) -def extensions_panel_draw_legacy_addons( - layout, - context, +# This function generalizes over displaying extension & legacy add-ons, +# (hence the many "item_*" arguments). +# Once support for legacy add-ons is dropped, the `item_` arguments +# can be replaced by a single `PkgManifest_Normalized` value. +def addon_draw_item_expanded( *, - search_lower, - extension_tags, - enabled_only, - used_addon_module_name_map, - addon_modules, + layout, # `bpy.types.UILayout` + mod, # `ModuleType` + addon_type, # `int` + is_enabled, # `bool` + # Expanded from both legacy add-ons & extensions. + item_name, # `str` + item_description, # `str` + item_maintainer, # `str` + item_version, # `str` + item_warning_legacy, # `str` + item_doc_url, # `str` + item_tracker_url, # `str` +): + from bpy.app.translations import ( + pgettext_iface as iface_, + ) + + 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=iface_("Built-in")) + rowsub.separator() + elif addon_type == ADDON_TYPE_LEGACY_USER: + rowsub.operator("preferences.addon_remove", text="Uninstall").module = mod.__name__ + del rowsub + + if item_doc_url or item_tracker_url: + sub = layout.split(factor=0.5) + if item_doc_url: + sub.operator( + "wm.url_open", text="Website", icon='HELP', + ).url = item_doc_url + else: + sub.separator() + + # Only add "Report a Bug" button if tracker_url is set + # or the add-on is bundled (use official tracker then). + if item_tracker_url: + sub.operator( + "wm.url_open", text="Report a Bug", icon='URL', + ).url = item_tracker_url + elif addon_type == ADDON_TYPE_LEGACY_CORE: + addon_info = ( + "Name: {:s} {:s}\n" + "Author: {:s}\n" + ).format(item_name, item_version, item_maintainer) + props = sub.operator( + "wm.url_open_preset", text="Report a Bug", icon='URL', + ) + props.type = 'BUG_ADDON' + props.id = addon_info + else: + sub.separator() + + sub = layout.box() + sub.active = is_enabled + split = sub.split(factor=0.125) + col_a = split.column() + col_b = split.column() + + col_a.alignment = 'RIGHT' + + 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_warning_legacy: + # Only for legacy add-ons. + col_a.label(text="Warning") + col_b.label(text=" " + item_warning_legacy, icon='ERROR', translate=False) + + col_a.label(text="File") + col_b.label(text=mod.__file__, translate=False) + + +# NOTE: this can be removed once upgrading from 4.1 is no longer relevant. +def addons_panel_draw_missing_with_extension_impl( + *, + context, # `bpy.types.Context` + layout, # `bpy.types.UILayout` + missing_modules # `Set[str]` +): + layout_header, layout_panel = layout.panel("builtin_addons", default_closed=True) + layout_header.label(text="Missing Built-in Add-ons", icon='ERROR') + + if layout_panel is None: + return + + prefs = context.preferences + extensions_map_from_legacy_addons_ensure() + + repo_index = -1 + repo = None + for repo_test_index, repo_test in enumerate(prefs.extensions.repos): + if ( + repo_test.use_remote_url and + (repo_test.remote_url.rstrip("/") == extensions_map_from_legacy_addons_url) + ): + repo_index = repo_test_index + repo = repo_test + break + + box = layout_panel.box() + box.label(text="Add-ons previously shipped with Blender are now available from extensions.blender.org.") + + if repo is None: + # Most likely the user manually removed this. + box.label(text="Blender's extension repository not found!", icon='ERROR') + elif not repo.enabled: + box.label(text="Blender's extension repository must be enabled to install extensions!", icon='ERROR') + repo_index = -1 + del repo + + for addon_module_name in sorted(missing_modules): + # The `addon_pkg_id` may be an empty string, this signifies that it's not mapped to an extension. + # The only reason to include it at all to avoid confusion because this *was* a previously built-in + # add-on and this panel is titled "Built-in Add-ons". + addon_pkg_id, addon_name = extensions_map_from_legacy_addons[addon_module_name] + + boxsub = box.column().box() + colsub = boxsub.column() + row = colsub.row() + + row_left = row.row() + row_left.alignment = 'LEFT' + + row_left.label(text=addon_name, translate=False) + + row_right = row.row() + row_right.alignment = 'RIGHT' + + if repo_index != -1 and addon_pkg_id: + # NOTE: it's possible this extension is already installed. + # the user could have installed it manually, then opened this popup. + # This is enough of a corner case that it's not especially worth detecting + # and communicating this particular state of affairs to the user. + # Worst case, they install and it will re-install an already installed extension. + props = row_right.operator("extensions.package_install", text="Install") + props.repo_index = repo_index + props.pkg_id = addon_pkg_id + props.do_legacy_replace = True + del props + + row_right.operator("preferences.addon_disable", text="", icon="X", emboss=False).module = addon_module_name + + +def addons_panel_draw_missing_impl( + *, + layout, # `bpy.types.UILayout` + missing_modules, # `Set[str]` +): + layout_header, layout_panel = layout.panel("missing_script_files", default_closed=True) + layout_header.label(text="Missing Add-ons", icon='ERROR') + + if layout_panel is None: + return + + box = layout_panel.box() + for addon_module_name in sorted(missing_modules): + + boxsub = box.column().box() + colsub = boxsub.column() + row = colsub.row(align=True) + + row_left = row.row() + row_left.alignment = 'LEFT' + row_left.label(text=addon_module_name, translate=False) + + row_right = row.row() + row_right.alignment = 'RIGHT' + row_right.operator("preferences.addon_disable", text="", icon="X", emboss=False).module = addon_module_name + + +def addons_panel_draw_items( + layout, # `bpy.types.UILayout` + context, # `bpy.types.Context` + *, + addon_modules, # `Dict[str, ModuleType]` + used_addon_module_name_map, # `Dict[str, bpy.types.Addon]` + search_lower, # `str` + addon_tags_exclude, # `Set[str]` + enabled_only, # `bool` + addon_extension_manifest_map, # `Dict[str, PkgManifest_Normalized]` ): # NOTE: this duplicates logic from `USERPREF_PT_addons` eventually this logic should be used instead. # Don't de-duplicate the logic as this is a temporary state - as long as extensions remains experimental. import addon_utils - from bpy.app.translations import ( - pgettext_iface as iface_, - pgettext_tip as tip_, - ) from .bl_extension_ops import ( pkg_info_check_exclude_filter_ex, ) @@ -118,33 +384,62 @@ def extensions_panel_draw_legacy_addons( for mod in addon_modules: module_name = mod.__name__ - is_extension = addon_utils.check_extension(module_name) - if is_extension: + is_enabled = module_name in used_addon_module_name_map + if enabled_only and (not is_enabled): continue + is_extension = addon_utils.check_extension(module_name) bl_info = addon_utils.module_bl_info(mod) + show_expanded = bl_info["show_expanded"] + + if is_extension: + del bl_info + item_local = addon_extension_manifest_map.get(module_name) + item_name = item_local.name + item_description = item_local.tagline + item_tags = item_local.tags + item_warning_legacy = "" + if show_expanded: + item_maintainer = item_local.maintainer + item_version = item_local.version + item_doc_url = item_local.website + item_tracker_url = "" + del item_local + else: + item_name = bl_info["name"] + item_description = bl_info["description"] + item_tags = (bl_info["category"],) + item_warning_legacy = bl_info["warning"] + if show_expanded: + item_maintainer = value.split("<", 1)[0].rstrip() if (value := bl_info["author"]) else "" + item_version = ".".join(str(x) for x in value) if (value := bl_info["version"]) else "" + item_doc_url = bl_info["doc_url"] + item_tracker_url = bl_info.get("tracker_url") + del bl_info + if search_lower and ( not pkg_info_check_exclude_filter_ex( - bl_info["name"], - bl_info["description"], + item_name, + item_description, search_lower, ) ): continue - is_enabled = module_name in used_addon_module_name_map - if enabled_only and (not is_enabled): - continue - - if extension_tags: - if t := bl_info.get("category"): - if extension_tags.get(t, True) is False: - continue - else: - # When extensions is not empty, skip items with empty tags. + if addon_tags_exclude: + if tags_exclude_match(item_tags, addon_tags_exclude): continue - del t + if is_extension: + addon_type = ADDON_TYPE_EXTENSION + elif module_parent_dirname(mod.__file__) == "addons_core": + addon_type = ADDON_TYPE_LEGACY_CORE + elif USERPREF_PT_addons.is_user_addon(mod, user_addon_paths): + addon_type = ADDON_TYPE_LEGACY_USER + else: + addon_type = ADDON_TYPE_LEGACY_OTHER + + # Draw header. col_box = layout.column() box = col_box.box() colsub = box.column() @@ -152,7 +447,7 @@ def extensions_panel_draw_legacy_addons( row.operator( "preferences.addon_expand", - icon='DOWNARROW_HLT' if bl_info["show_expanded"] else 'RIGHTARROW', + icon='DOWNARROW_HLT' if show_expanded else 'RIGHTARROW', emboss=False, ).module = module_name @@ -165,88 +460,182 @@ def extensions_panel_draw_legacy_addons( sub = row.row() sub.active = is_enabled - if module_parent_dirname(mod.__file__) == "addons_core": - sub.label(text=iface_("Core:") + " " + iface_(bl_info["name"]), translate=False) - else: - sub.label(text=iface_("Legacy:") + " " + iface_(bl_info["name"]), translate=False) + sub.label(text=" " + item_name, translate=False) - if bl_info["warning"]: + if item_warning_legacy: sub.label(icon='ERROR') + elif USE_SHOW_ADDON_TYPE_AS_ICON: + sub.label(icon=addon_type_icon[addon_type]) - row_right = row.row() - row_right.alignment = 'RIGHT' - - row_right.label(text="Installed ") - row_right.active = False - - if bl_info["show_expanded"]: - user_addon = USERPREF_PT_addons.is_user_addon(mod, user_addon_paths) - - split = box.split(factor=0.8) - col_a = split.column() - col_b = split.column() - - if bl_info["description"]: - col_a.label( - text="{:s}.".format(tip_(bl_info["description"])), - translate=False, - ) - - if bl_info["doc_url"] or bl_info.get("tracker_url"): - sub = box.row() - if bl_info["doc_url"]: - sub.operator( - "wm.url_open", text="Documentation", icon='HELP', - ).url = bl_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 bl_info.get("tracker_url"): - sub.operator( - "wm.url_open", text="Report a Bug", icon='URL', - ).url = bl_info["tracker_url"] - elif not user_addon: - addon_info = ( - "Name: %s %s\n" - "Author: %s\n" - ) % (bl_info["name"], str(bl_info["version"]), bl_info["author"]) - props = sub.operator( - "wm.url_open_preset", text="Report a Bug", icon='URL', - ) - props.type = 'BUG_ADDON' - props.id = addon_info - - sub = box.box() - sub.active = is_enabled - split = sub.split(factor=0.125) - col_a = split.column() - col_b = split.column() - - col_a.alignment = 'RIGHT' - if value := bl_info["author"]: - col_a.label(text="Maintainer") - col_b.label(text=value.split("<", 1)[0].rstrip(), translate=False) - if value := bl_info["version"]: - col_a.label(text="Version") - col_b.label(text=".".join(str(x) for x in value), translate=False) - if value := bl_info["warning"]: - col_a.label(text="Warning") - col_b.label(text=" " + iface_(value), icon='ERROR', translate=False) - del value - - col_a.label(text="File") - col_b.label(text=mod.__file__, translate=False) - - if user_addon: - rowsub = col_b.row() - rowsub.alignment = 'RIGHT' - rowsub.operator( - "preferences.addon_remove", text="Uninstall", icon='CANCEL', - ).module = module_name + if show_expanded: + addon_draw_item_expanded( + layout=box, + mod=mod, + addon_type=addon_type, + is_enabled=is_enabled, + # Expanded from both legacy add-ons & extensions. + item_name=item_name, + item_description=item_description, + item_maintainer=item_maintainer, + item_version=item_version, + item_warning_legacy=item_warning_legacy, + item_doc_url=item_doc_url, + item_tracker_url=item_tracker_url, + ) if is_enabled: if (addon_preferences := used_addon_module_name_map[module_name].preferences) is not None: + box = layout.box() USERPREF_PT_addons.draw_addon_preferences(box, context, addon_preferences) - del sub + + +def addons_panel_draw_impl( + self, + context, # `bpy.types.Context` + search_lower, # `str` + addon_tags_exclude, # `Set[str]` + enabled_only, # `bool` +): + """ + Show all the items... we may want to paginate at some point. + """ + import addon_utils + from .bl_extension_ops import ( + extension_repos_read, + repo_cache_store_refresh_from_prefs, + ) + + from . import repo_cache_store_ensure + + repo_cache_store = repo_cache_store_ensure() + + # This isn't elegant, but the preferences aren't available on registration. + if not repo_cache_store.is_init(): + repo_cache_store_refresh_from_prefs(repo_cache_store) + + layout = self.layout + + prefs = context.preferences + + # Define a top-most column to place warnings (if-any). + # Needed so the warnings aren't mixed in with other content. + layout_topmost = layout.column() + + repos_all = extension_repos_read() + + # Collect exceptions accessing repositories, and optionally show them. + errors_on_draw = [] + + local_ex = None + + def error_fn_local(ex): + nonlocal local_ex + local_ex = ex + + addon_extension_manifest_map = {} + + for repo_index, pkg_manifest_local in enumerate( + repo_cache_store.pkg_manifest_from_local_ensure(error_fn=error_fn_local) + ): + if pkg_manifest_local is None: + continue + + repo_module_prefix = pkg_repo_module_prefix(repos_all[repo_index]) + for pkg_id, item_local in pkg_manifest_local.items(): + if item_local.type != "add-on": + continue + module_name = repo_module_prefix + pkg_id + addon_extension_manifest_map[module_name] = item_local + + addon_modules = addon_utils.modules(refresh=False) + used_addon_module_name_map = {addon.module: addon for addon in prefs.addons} + + addons_panel_draw_items( + layout, + context, + addon_modules=addon_modules, + used_addon_module_name_map=used_addon_module_name_map, + search_lower=search_lower, + addon_tags_exclude=addon_tags_exclude, + enabled_only=enabled_only, + addon_extension_manifest_map=addon_extension_manifest_map, + ) + + # Append missing scripts + # First collect scripts that are used but have no script file. + module_names = {mod.__name__ for mod in addon_modules} + missing_modules = { + addon_module_name for addon_module_name in used_addon_module_name_map + if addon_module_name not in module_names + } + + # NOTE: this can be removed once upgrading from 4.1 is no longer relevant. + if missing_modules: + # Split the missing modules into two groups, ones which can be upgraded and ones that can't. + extensions_map_from_legacy_addons_ensure() + + missing_modules_with_extension = set() + missing_modules_without_extension = set() + + for addon_module_name in missing_modules: + if addon_module_name in extensions_map_from_legacy_addons: + missing_modules_with_extension.add(addon_module_name) + else: + missing_modules_without_extension.add(addon_module_name) + if missing_modules_with_extension: + addons_panel_draw_missing_with_extension_impl( + context=context, + layout=layout_topmost, + missing_modules=missing_modules_with_extension, + ) + + # Pretend none of these shenanigans ever occurred (to simplify removal). + missing_modules = missing_modules_without_extension + # End code-path for 4.1x migration. + + if missing_modules: + addons_panel_draw_missing_impl( + layout=layout_topmost, + missing_modules=missing_modules, + ) + + # Finally show any errors in a single panel which can be dismissed. + display_errors.errors_curr = errors_on_draw + if errors_on_draw: + display_errors.draw(layout_topmost) + + +def addons_panel_draw(panel, context): + prefs = context.preferences + view = prefs.view + + wm = context.window_manager + layout = panel.layout + + split = layout.split(factor=0.5) + row_a = split.row() + row_b = split.row() + row_a.prop(wm, "addon_search", text="", icon='VIEWZOOM') + row_b.prop(view, "show_addons_enabled_only") + rowsub = row_b.row(align=True) + + rowsub.popover("USERPREF_PT_addons_tags", text="", icon='TAG') + + rowsub.separator() + + rowsub.menu("USERPREF_MT_addons_settings", text="", icon='DOWNARROW_HLT') + del split, row_a, row_b, rowsub + + # Create a set of tags marked False to simplify exclusion & avoid it altogether when all tags are enabled. + addon_tags_exclude = {k for (k, v) in wm.get("addon_tags", {}).items() if v is False} + + addons_panel_draw_impl( + panel, + context, + wm.addon_search.lower(), + addon_tags_exclude, + view.show_addons_enabled_only, + ) # ----------------------------------------------------------------------------- @@ -397,145 +786,23 @@ def extensions_map_from_legacy_addons_reverse_lookup(pkg_id): return "" -# NOTE: this can be removed once upgrading from 4.1 is no longer relevant. -def extensions_panel_draw_missing_with_extension_impl( - *, - context, - layout, - missing_modules, -): - layout_header, layout_panel = layout.panel("builtin_addons", default_closed=True) - layout_header.label(text="Missing Built-in Add-ons", icon='ERROR') - - if layout_panel is None: - return - - prefs = context.preferences - extensions_map_from_legacy_addons_ensure() - - repo_index = -1 - repo = None - for repo_test_index, repo_test in enumerate(prefs.extensions.repos): - if ( - repo_test.use_remote_url and - (repo_test.remote_url.rstrip("/") == extensions_map_from_legacy_addons_url) - ): - repo_index = repo_test_index - repo = repo_test - break - - box = layout_panel.box() - box.label(text="Add-ons previously shipped with Blender are now available from extensions.blender.org.") - - if repo is None: - # Most likely the user manually removed this. - box.label(text="Blender's extension repository not found!", icon='ERROR') - elif not repo.enabled: - box.label(text="Blender's extension repository must be enabled to install extensions!", icon='ERROR') - repo_index = -1 - del repo - - for addon_module_name in sorted(missing_modules): - # The `addon_pkg_id` may be an empty string, this signifies that it's not mapped to an extension. - # The only reason to include it at all to avoid confusion because this *was* a previously built-in - # add-on and this panel is titled "Built-in Add-ons". - addon_pkg_id, addon_name = extensions_map_from_legacy_addons[addon_module_name] - - boxsub = box.column().box() - colsub = boxsub.column() - row = colsub.row() - - row_left = row.row() - row_left.alignment = 'LEFT' - - row_left.label(text=addon_name, translate=False) - - row_right = row.row() - row_right.alignment = 'RIGHT' - - if repo_index != -1 and addon_pkg_id: - # NOTE: it's possible this extension is already installed. - # the user could have installed it manually, then opened this popup. - # This is enough of a corner case that it's not especially worth detecting - # and communicating this particular state of affairs to the user. - # Worst case, they install and it will re-install an already installed extension. - props = row_right.operator("extensions.package_install", text="Install") - props.repo_index = repo_index - props.pkg_id = addon_pkg_id - props.do_legacy_replace = True - del props - - row_right.operator("preferences.addon_disable", text="", icon="X", emboss=False).module = addon_module_name - - -def extensions_panel_draw_missing_impl( - *, - layout, - missing_modules, -): - layout_header, layout_panel = layout.panel("missing_script_files", default_closed=True) - layout_header.label(text="Missing Add-ons", icon='ERROR') - - if layout_panel is None: - return - - box = layout_panel.box() - for addon_module_name in sorted(missing_modules): - - boxsub = box.column().box() - colsub = boxsub.column() - row = colsub.row(align=True) - - row_left = row.row() - row_left.alignment = 'LEFT' - row_left.label(text=addon_module_name, translate=False) - - row_right = row.row() - row_right.alignment = 'RIGHT' - row_right.operator("preferences.addon_disable", text="", icon="X", emboss=False).module = addon_module_name - - -def pkg_manifest_zip_all_items(pkg_manifest_local, pkg_manifest_remote): - if pkg_manifest_remote is None: - if pkg_manifest_local is not None: - for pkg_id, item_local in pkg_manifest_local.items(): - yield pkg_id, (item_local, None) - # If both are none, there are no items, that's OK. - return - elif pkg_manifest_local is None: - for pkg_id, item_remote in pkg_manifest_remote.items(): - yield pkg_id, (None, item_remote) - return - - assert (pkg_manifest_remote is not None) and (pkg_manifest_local is not None) - pkg_manifest_local_copy = pkg_manifest_local.copy() - for pkg_id, item_remote in pkg_manifest_remote.items(): - yield pkg_id, (pkg_manifest_local_copy.pop(pkg_id, None), item_remote) - # Orphan packages (if they exist). - for pkg_id, item_local in pkg_manifest_local_copy.items(): - yield pkg_id, (item_local, None) - - def extensions_panel_draw_impl( self, - context, - search_lower, - filter_by_type, - extension_tags, - enabled_only, - updates_only, - installed_only, - show_legacy_addons, - show_development, + context, # `bpy.types.Context` + search_lower, # `str` + filter_by_type, # `str` + extension_tags_exclude, # `Set[str]` + enabled_only, # `bool` + updates_only, # `bool` + installed_only, # `bool` + show_development, # `bool` ): """ Show all the items... we may want to paginate at some point. """ - import addon_utils import os from bpy.app.translations import ( pgettext_iface as iface_, - pgettext_tip as tip_, ) from .bl_extension_ops import ( blender_extension_mark, @@ -559,7 +826,6 @@ def extensions_panel_draw_impl( if updates_only: installed_only = True - show_legacy_addons = False # Define a top-most column to place warnings (if-any). # Needed so the warnings aren't mixed in with other content. @@ -581,7 +847,6 @@ def extensions_panel_draw_impl( show_themes = filter_by_type in {"", "theme"} if show_addons: used_addon_module_name_map = {addon.module: addon for addon in prefs.addons} - addon_modules = addon_utils.modules(refresh=False) if show_themes: active_theme_info = pkg_repo_and_id_from_theme_path(repos_all, prefs.themes[0].filepath) @@ -649,6 +914,8 @@ def extensions_panel_draw_impl( # Read-only. is_system_repo = repos_all[repo_index].source == 'SYSTEM' + repo_module_prefix = pkg_repo_module_prefix(repos_all[repo_index]) + for pkg_id, (item_local, item_remote) in pkg_manifest_zip_all_items(pkg_manifest_local, pkg_manifest_remote): item = item_local or item_remote if filter_by_type and (filter_by_type != item.type): @@ -661,12 +928,8 @@ def extensions_panel_draw_impl( if installed_only and (is_installed == 0): continue - if extension_tags: - if tags := item.tags: - if not any(True for t in tags if extension_tags.get(t, True)): - continue - else: - # When extensions is not empty, skip items with empty tags. + if extension_tags_exclude: + if tags_exclude_match(item.tags, extension_tags_exclude): continue is_addon = False @@ -680,7 +943,7 @@ def extensions_panel_draw_impl( if is_addon: if is_installed: # Currently we only need to know the module name once installed. - addon_module_name = "bl_ext.{:s}.{:s}".format(repos_all[repo_index].module, pkg_id) + addon_module_name = repo_module_prefix + pkg_id # pylint: disable-next=possibly-used-before-assignment is_enabled = addon_module_name in used_addon_module_name_map @@ -731,36 +994,43 @@ def extensions_panel_draw_impl( props.repo_index = repo_index del props - if is_installed: - if is_addon: - 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 = addon_module_name - elif is_theme: - props = row.operator( - "extensions.package_theme_disable" if is_enabled else "extensions.package_theme_enable", - icon='CHECKBOX_HLT' if is_enabled else 'CHECKBOX_DEHLT', - text="", - emboss=False, - ) - props.repo_index = repo_index - props.pkg_id = pkg_id - del props + if USE_EXTENSIONS_CHECKBOX: + if is_installed: + if is_addon: + 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 = addon_module_name + elif is_theme: + props = row.operator( + "extensions.package_theme_disable" if is_enabled else "extensions.package_theme_enable", + icon='CHECKBOX_HLT' if is_enabled else 'CHECKBOX_DEHLT', + text="", + emboss=False, + ) + props.repo_index = repo_index + props.pkg_id = pkg_id + del props + else: + # Use a place-holder checkbox icon to avoid odd text alignment + # when mixing with installed add-ons. + # Non add-ons have no concept of "enabled" right now, use installed. + row.operator( + "extensions.package_disabled", + text="", + icon='CHECKBOX_HLT', + emboss=False, + ) else: - # Use a place-holder checkbox icon to avoid odd text alignment when mixing with installed add-ons. - # Non add-ons have no concept of "enabled" right now, use installed. + # Not installed, always placeholder. row.operator( - "extensions.package_disabled", + "extensions.package_enable_not_installed", text="", - icon='CHECKBOX_HLT', + icon='CHECKBOX_DEHLT', emboss=False, ) - else: - # Not installed, always placeholder. - row.operator("extensions.package_enable_not_installed", text="", icon='CHECKBOX_DEHLT', emboss=False) if show_development: if mark: @@ -773,7 +1043,19 @@ def extensions_panel_draw_impl( sub = row.row() sub.active = is_enabled - sub.label(text=item.name, translate=False) + if USE_EXTENSIONS_CHECKBOX: + sub.label(text=item.name, translate=False) + else: + # Without checking `is_enabled` here, there is no way for the user to know if an extension + # is enabled or not, which is useful to show - when they may be considering removing/updating + # extensions based on them being used or not. + sub.label( + text=( + item.name if (is_enabled or is_installed is False) else + item.name + iface_(" [disabled]") + ), + translate=False, + ) del sub row_right = row.row(align=True) @@ -813,7 +1095,7 @@ def extensions_panel_draw_impl( col_b = split.column() # The full tagline may be multiple lines (not yet supported by Blender's UI). - col_a.label(text="{:s}.".format(tip_(item.tagline)), translate=False) + col_a.label(text="{:s}.".format(item.tagline), translate=False) if value := item.website: # Use half size button, for legacy add-ons there are two, here there is one @@ -887,66 +1169,16 @@ def extensions_panel_draw_impl( col_b.label(text=os.path.join(repos_all[repo_index].directory, pkg_id), translate=False) # Show addon user preferences. - if is_enabled and is_addon: - if (addon_preferences := used_addon_module_name_map[addon_module_name].preferences) is not None: - USERPREF_PT_addons.draw_addon_preferences(box, context, addon_preferences) - - if show_addons and show_legacy_addons: - extensions_panel_draw_legacy_addons( - layout, - context, - search_lower=search_lower, - extension_tags=extension_tags, - enabled_only=enabled_only, - used_addon_module_name_map=used_addon_module_name_map, - # pylint: disable-next=possibly-used-before-assignment - addon_modules=addon_modules, - ) + if USE_EXTENSIONS_ADDON_PREFS: + if is_enabled and is_addon: + if (addon_preferences := used_addon_module_name_map[addon_module_name].preferences) is not None: + USERPREF_PT_addons.draw_addon_preferences(box, context, addon_preferences) # Finally show any errors in a single panel which can be dismissed. display_errors.errors_curr = errors_on_draw if errors_on_draw: display_errors.draw(layout_topmost) - # Append missing scripts - # First collect scripts that are used but have no script file. - if show_addons: - module_names = {mod.__name__ for mod in addon_modules} - missing_modules = { - addon_module_name for addon_module_name in used_addon_module_name_map - if addon_module_name not in module_names - } - - # NOTE: this can be removed once upgrading from 4.1 is no longer relevant. - if missing_modules: - # Split the missing modules into two groups, ones which can be upgraded and ones that can't. - extensions_map_from_legacy_addons_ensure() - - missing_modules_with_extension = set() - missing_modules_without_extension = set() - - for addon_module_name in missing_modules: - if addon_module_name in extensions_map_from_legacy_addons: - missing_modules_with_extension.add(addon_module_name) - else: - missing_modules_without_extension.add(addon_module_name) - if missing_modules_with_extension: - extensions_panel_draw_missing_with_extension_impl( - context=context, - layout=layout_topmost, - missing_modules=missing_modules_with_extension, - ) - - # Pretend none of these shenanigans ever occurred (to simplify removal). - missing_modules = missing_modules_without_extension - # End code-path for 4.1x migration. - - if missing_modules: - extensions_panel_draw_missing_impl( - layout=layout_topmost, - missing_modules=missing_modules, - ) - class USERPREF_PT_extensions_filter(Panel): bl_label = "Extensions Filter" @@ -968,11 +1200,19 @@ class USERPREF_PT_extensions_filter(Panel): sub.active = (not wm.extension_enabled_only) and (not wm.extension_updates_only) sub.prop(wm, "extension_installed_only", text="Installed Extensions") - col = layout.column(heading="Show") - col.use_property_split = True - sub = col.column() - sub.active = not wm.extension_updates_only - sub.prop(wm, "extension_show_legacy_addons", text="Legacy Add-ons") + +class USERPREF_PT_addons_tags(Panel): + bl_label = "Addon Tags" + + bl_space_type = 'TOPBAR' # dummy. + bl_region_type = 'HEADER' + bl_ui_units_x = 13 + + _wm_tags_attr = "addon_tags" + + def draw(self, _context): + # Extended by the `bl_pkg` add-on. + pass class USERPREF_PT_extensions_tags(Panel): @@ -982,11 +1222,26 @@ class USERPREF_PT_extensions_tags(Panel): bl_region_type = 'HEADER' bl_ui_units_x = 13 + _wm_tags_attr = "extension_tags" + def draw(self, _context): # Extended by the `bl_pkg` add-on. pass +class USERPREF_MT_addons_settings(Menu): + bl_label = "Add-ons Settings" + + def draw(self, _context): + layout = self.layout + + layout.operator("extensions.repo_refresh_all", text="Refresh Local", icon='FILE_REFRESH') + + layout.separator() + + layout.operator("extensions.package_install_files", text="Install from Disk...") + + class USERPREF_MT_extensions_settings(Menu): bl_label = "Extension Settings" @@ -1124,120 +1379,22 @@ def extensions_panel_draw(panel, context): ): extensions_panel_draw_online_extensions_request_impl(panel, context) - if extension_tags := wm.get("extension_tags", {}): - # Filter out true items, so an empty dict can always be skipped. - extension_tags = {k: v for (k, v) in extension_tags.items() if v is False} + # Create a set of tags marked False to simplify exclusion & avoid it altogether when all tags are enabled. + extension_tags_exclude = {k for (k, v) in wm.get("extension_tags", {}).items() if v is False} extensions_panel_draw_impl( panel, context, wm.extension_search.lower(), blender_filter_by_type_map[wm.extension_type], - extension_tags, + extension_tags_exclude, wm.extension_enabled_only, wm.extension_updates_only, wm.extension_installed_only, - wm.extension_show_legacy_addons, show_development, ) -def tags_current(wm): - from .bl_extension_ops import ( - blender_filter_by_type_map, - repo_cache_store_refresh_from_prefs, - ) - - from . import repo_cache_store_ensure - - repo_cache_store = repo_cache_store_ensure() - - # This isn't elegant, but the preferences aren't available on registration. - if not repo_cache_store.is_init(): - repo_cache_store_refresh_from_prefs(repo_cache_store) - - filter_by_type = blender_filter_by_type_map[wm.extension_type] - - tags = set() - - for ( - pkg_manifest_local, - pkg_manifest_remote, - ) in zip( - repo_cache_store.pkg_manifest_from_local_ensure(error_fn=print), - repo_cache_store.pkg_manifest_from_remote_ensure(error_fn=print), - strict=True, - ): - for pkg_id, (item_local, item_remote) in pkg_manifest_zip_all_items(pkg_manifest_local, pkg_manifest_remote): - item = item_local or item_remote - if filter_by_type != item.type: - continue - if pkg_tags := item.tags: - tags.update(pkg_tags) - - if filter_by_type == "add-on": - # Legacy add-on categories as tags. - import addon_utils - addon_modules = addon_utils.modules(refresh=False) - for mod in addon_modules: - module_name = mod.__name__ - is_extension = addon_utils.check_extension(module_name) - if is_extension: - continue - bl_info = addon_utils.module_bl_info(mod) - if t := bl_info.get("category"): - tags.add(t) - - return tags - - -def tags_refresh(wm): - import idprop - tags_idprop = wm.get("extension_tags") - if isinstance(tags_idprop, idprop.types.IDPropertyGroup): - pass - else: - wm["extension_tags"] = {} - tags_idprop = wm["extension_tags"] - - tags_curr = set(tags_idprop.keys()) - - # Calculate tags. - tags_next = tags_current(wm) - - tags_to_add = tags_next - tags_curr - tags_to_rem = tags_curr - tags_next - - for tag in tags_to_rem: - del tags_idprop[tag] - for tag in tags_to_add: - tags_idprop[tag] = True - - return list(sorted(tags_next)) - - -def tags_panel_draw(panel, context): - from bpy.utils import escape_identifier - from bpy.app.translations import contexts as i18n_contexts - layout = panel.layout - wm = context.window_manager - tags_sorted = tags_refresh(wm) - layout.label(text="Show Tags") - # Add one so the first row is longer in the case of an odd number. - tags_len_half = (len(tags_sorted) + 1) // 2 - split = layout.split(factor=0.5) - col = split.column() - for i, t in enumerate(sorted(tags_sorted)): - if i == tags_len_half: - col = split.column() - col.prop( - wm.extension_tags, - "[\"{:s}\"]".format(escape_identifier(t)), - text=t, - text_ctxt=i18n_contexts.editor_preferences, - ) - - def extensions_repo_active_draw(self, _context): # Draw icon buttons on the right hand side of the UI-list. from . import repo_active_or_none @@ -1252,8 +1409,170 @@ def extensions_repo_active_draw(self, _context): layout.operator("extensions.package_upgrade_all", text="", icon='IMPORT').use_active_only = True +# ----------------------------------------------------------------------------- +# Shared (Extension / Legacy Add-ons) Tags Logic + +def tags_exclude_match( + item_tags, # `Tuple[str]` + exclude_tags, # `Set[str]` +): + if not item_tags: + # When an item has no tags then including it makes no sense + # since this item logically can't match any of the enabled tags. + # skip items with no empty tags - when tags are being filtered. + return True + + for tag in item_tags: + # Any tag not in `exclude_tags` is assumed true. + # This works because `exclude_tags` is a complete list of all tags. + if tag not in exclude_tags: + return False + return True + + +def tags_current(wm, tags_attr): + from .bl_extension_ops import ( + blender_filter_by_type_map, + extension_repos_read, + repo_cache_store_refresh_from_prefs, + ) + + from . import repo_cache_store_ensure + + prefs = bpy.context.preferences + + repo_cache_store = repo_cache_store_ensure() + + # This isn't elegant, but the preferences aren't available on registration. + if not repo_cache_store.is_init(): + repo_cache_store_refresh_from_prefs(repo_cache_store) + + if tags_attr == "addon_tags": + filter_by_type = "add-on" + only_enabled = prefs.view.show_addons_enabled_only + else: + filter_by_type = blender_filter_by_type_map[wm.extension_type] + only_enabled = wm.extension_enabled_only + + # Currently only add-ons can make use of enabled by type (usefully) for tags. + if only_enabled and (filter_by_type == "add-on"): + addons_enabled = {addon.module for addon in prefs.addons} + + repos_all = extension_repos_read() + + tags = set() + + for repo_index, ( + pkg_manifest_local, + pkg_manifest_remote, + ) in enumerate(zip( + repo_cache_store.pkg_manifest_from_local_ensure(error_fn=print), + repo_cache_store.pkg_manifest_from_remote_ensure(error_fn=print) if (tags_attr != "addon_tags") else + # For add-ons display there is never any need for "remote" items, + # simply expand to None here to avoid duplicating the body of this for-loop. + ((None,) * len(repos_all)), + strict=True, + )): + if only_enabled: + if filter_by_type == "add-on": + repo_module_prefix = pkg_repo_module_prefix(repos_all[repo_index]) + + for pkg_id, (item_local, item_remote) in pkg_manifest_zip_all_items(pkg_manifest_local, pkg_manifest_remote): + item = item_local or item_remote + + # Filter using `filter_by_type`. + if filter_by_type != item.type: + continue + + # Filter using `Only Enabled`. + # NOTE: this is only supported by add-ons currently. + # This could be made to work for themes too however there is only ever one enabled theme at a time. + # The use case for that is weak at best. "Only Enabled" can be supported by other types as needed. + if only_enabled: + if filter_by_type == "add-on": + if item_local is not None: + addon_module_name = repo_module_prefix + pkg_id + if addon_module_name not in addons_enabled: + continue + if pkg_tags := item.tags: + tags.update(pkg_tags) + + # Only for the add-ons view (extension's doesn't show legacy add-ons). + if tags_attr == "addon_tags": + # Legacy add-on categories as tags. + import addon_utils + addon_modules = addon_utils.modules(refresh=False) + for mod in addon_modules: + module_name = mod.__name__ + is_extension = addon_utils.check_extension(module_name) + if is_extension: + continue + if only_enabled: # No need to check `filter_by_type` here. + if module_name not in addons_enabled: + continue + bl_info = addon_utils.module_bl_info(mod) + if t := bl_info.get("category"): + tags.add(t) + + return tags + + +def tags_refresh(wm, tags_attr): + import idprop + tags_idprop = wm.get(tags_attr) + if isinstance(tags_idprop, idprop.types.IDPropertyGroup): + pass + else: + wm[tags_attr] = {} + tags_idprop = wm[tags_attr] + + tags_curr = set(tags_idprop.keys()) + + # Calculate tags. + tags_next = tags_current(wm, tags_attr) + + tags_to_add = tags_next - tags_curr + tags_to_rem = tags_curr - tags_next + + for tag in tags_to_rem: + del tags_idprop[tag] + for tag in tags_to_add: + tags_idprop[tag] = True + + return list(sorted(tags_next)) + + +def tags_panel_draw(panel, context): + tags_attr = panel._wm_tags_attr + from bpy.utils import escape_identifier + from bpy.app.translations import contexts as i18n_contexts + layout = panel.layout + wm = context.window_manager + tags_sorted = tags_refresh(wm, tags_attr) + layout.label(text="Show Tags") + # Add one so the first row is longer in the case of an odd number. + tags_len_half = (len(tags_sorted) + 1) // 2 + split = layout.split(factor=0.5) + col = split.column() + for i, t in enumerate(sorted(tags_sorted)): + if i == tags_len_half: + col = split.column() + col.prop( + getattr(wm, tags_attr), + "[\"{:s}\"]".format(escape_identifier(t)), + text=t, + text_ctxt=i18n_contexts.editor_preferences, + ) + + +# ----------------------------------------------------------------------------- +# Registration + classes = ( # Pop-overs. + USERPREF_PT_addons_tags, + USERPREF_MT_addons_settings, + USERPREF_PT_extensions_filter, USERPREF_PT_extensions_tags, USERPREF_MT_extensions_settings, @@ -1261,7 +1580,9 @@ classes = ( def register(): - USERPREF_PT_addons.append(extensions_panel_draw) + USERPREF_PT_addons.append(addons_panel_draw) + USERPREF_PT_extensions.append(extensions_panel_draw) + USERPREF_PT_addons_tags.append(tags_panel_draw) USERPREF_PT_extensions_tags.append(tags_panel_draw) USERPREF_MT_extensions_active_repo.append(extensions_repo_active_draw) @@ -1270,8 +1591,10 @@ def register(): def unregister(): - USERPREF_PT_addons.remove(extensions_panel_draw) + USERPREF_PT_addons.remove(addons_panel_draw) + USERPREF_PT_extensions.remove(extensions_panel_draw) USERPREF_PT_extensions_tags.remove(tags_panel_draw) + USERPREF_PT_addons_tags.remove(tags_panel_draw) USERPREF_MT_extensions_active_repo.remove(extensions_repo_active_draw) for cls in reversed(classes): diff --git a/scripts/startup/bl_operators/userpref.py b/scripts/startup/bl_operators/userpref.py index 8bd00a14adf..6d9911f0f0e 100644 --- a/scripts/startup/bl_operators/userpref.py +++ b/scripts/startup/bl_operators/userpref.py @@ -888,7 +888,7 @@ class PREFERENCES_OT_addon_show(Operator): bl_info = addon_utils.module_bl_info(mod) bl_info["show_expanded"] = True - context.preferences.active_section = 'EXTENSIONS' + context.preferences.active_section = 'ADDONS' context.preferences.view.show_addons_enabled_only = False context.window_manager.addon_filter = 'All' context.window_manager.addon_search = bl_info["name"] diff --git a/scripts/startup/bl_ui/space_userpref.py b/scripts/startup/bl_ui/space_userpref.py index 87c616015b0..31094f51384 100644 --- a/scripts/startup/bl_ui/space_userpref.py +++ b/scripts/startup/bl_ui/space_userpref.py @@ -2261,6 +2261,23 @@ class USERPREF_PT_extensions_repos(Panel): layout_panel.prop(active_repo, "module") +# ----------------------------------------------------------------------------- +# Extensions Panels + +class ExtensionsPanel: + bl_space_type = 'PREFERENCES' + bl_region_type = 'WINDOW' + bl_context = "extensions" + + +class USERPREF_PT_extensions(ExtensionsPanel, Panel): + bl_label = "Extensions" + bl_options = {'HIDE_HEADER'} + + def draw(self, context): + pass + + # ----------------------------------------------------------------------------- # Add-on Panels @@ -2279,7 +2296,7 @@ class USERPREF_PT_addons_filter(Panel): class AddOnPanel: bl_space_type = 'PREFERENCES' bl_region_type = 'WINDOW' - bl_context = "extensions" + bl_context = "addons" class USERPREF_PT_addons(AddOnPanel, Panel): @@ -2950,6 +2967,7 @@ classes = ( USERPREF_PT_keymap, + USERPREF_PT_extensions, USERPREF_PT_addons, USERPREF_MT_extensions_active_repo, diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index 05ede7bd008..7c25282718c 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -1142,7 +1142,7 @@ typedef enum eUserPref_Section { USER_SECTION_SYSTEM = 3, USER_SECTION_THEME = 4, USER_SECTION_INPUT = 5, - USER_SECTION_EXTENSIONS = 6, + USER_SECTION_ADDONS = 6, USER_SECTION_LIGHT = 7, USER_SECTION_KEYMAP = 8, #ifdef WITH_USERDEF_WORKSPACES @@ -1155,6 +1155,7 @@ typedef enum eUserPref_Section { USER_SECTION_NAVIGATION = 14, USER_SECTION_FILE_PATHS = 15, USER_SECTION_EXPERIMENTAL = 16, + USER_SECTION_EXTENSIONS = 17, } eUserPref_Section; /** #UserDef_SpaceData.flag (State of the user preferences UI). */ diff --git a/source/blender/makesrna/intern/rna_userdef.cc b/source/blender/makesrna/intern/rna_userdef.cc index 399ceab3369..90a766e8c93 100644 --- a/source/blender/makesrna/intern/rna_userdef.cc +++ b/source/blender/makesrna/intern/rna_userdef.cc @@ -60,6 +60,7 @@ const EnumPropertyItem rna_enum_preference_section_items[] = { {USER_SECTION_ANIMATION, "ANIMATION", 0, "Animation", ""}, RNA_ENUM_ITEM_SEPR, {USER_SECTION_EXTENSIONS, "EXTENSIONS", 0, "Extensions", ""}, + {USER_SECTION_ADDONS, "ADDONS", 0, "Add-ons", ""}, {USER_SECTION_THEME, "THEMES", 0, "Themes", ""}, #if 0 /* def WITH_USERDEF_WORKSPACES */ RNA_ENUM_ITEM_SEPR,