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