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.
This commit is contained in:
Campbell Barton
2024-06-21 10:42:53 +10:00
parent b89c7635d2
commit 72ef03d5a1
6 changed files with 783 additions and 442 deletions

View File

@@ -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)

View File

@@ -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):

View File

@@ -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"]

View File

@@ -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,

View File

@@ -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). */

View File

@@ -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,