From 57ff2a566482f07cf05016724976e9fb27220bcb Mon Sep 17 00:00:00 2001 From: Ellwood Zwovic Date: Wed, 26 Jul 2017 23:36:14 -0700 Subject: [PATCH] Clean up action button display, and add enabling/disabling --- package_manager/__init__.py | 285 ++++++++++++++++++++++++---------- package_manager/bpkg/types.py | 3 + package_manager/utils.py | 7 + 3 files changed, 213 insertions(+), 82 deletions(-) diff --git a/package_manager/__init__.py b/package_manager/__init__.py index dac7ccb..f7c4872 100644 --- a/package_manager/__init__.py +++ b/package_manager/__init__.py @@ -57,6 +57,7 @@ class ConsolidatedPackage: def __init__(self, pkg=None): self.versions = [] + self.updateable = False if pkg is not None: self.add_version(pkg) @@ -69,6 +70,14 @@ class ConsolidatedPackage: return True return False + @property + def name(self) -> str: + """ + Return name of this package. All package versions in a + ConsolidatedPackage should have the same name by definition + """ + return self.versions[0].name + def get_latest_installed_version(self) -> bpkg.Package: """ Return the installed package with the highest version number. @@ -84,6 +93,16 @@ class ConsolidatedPackage: """Return package with highest version number""" return self.versions[0] # this is always sorted with the highest on top + def get_display_version(self) -> bpkg.Package: + """ + Return installed package with highest version number. + If no version is installed, return highest uninstalled version. + """ + pkg = self.get_latest_installed_version() + if pkg is None: + pkg = self.get_latest_version() + return pkg + def add_version(self, pkg: Package): self.versions.append(pkg) self.versions.sort(key=lambda v: v.version, reverse=True) @@ -305,7 +324,7 @@ class PACKAGE_OT_install(SubprocMixin, bpy.types.Operator): class PACKAGE_OT_uninstall(SubprocMixin, bpy.types.Operator): bl_idname = 'package.uninstall' bl_label = 'Install package' - bl_description = 'Downloads and installs a Blender add-on package' + bl_description = "Remove installed package files from filesystem" bl_options = {'REGISTER'} package_name = bpy.props.StringProperty(name='package_name', description='The name of the package to uninstall') @@ -607,7 +626,7 @@ class USERPREF_PT_packages(bpy.types.Panel): spl_r = spl.row() spl_r.prop(wm, "package_install_filter", expand=True) - def filtered_packages(filters: dict, packages: OrderedDict) -> list: + def filtered_packages(filters: dict, packages: OrderedDict) -> list:# {{{ """Returns filtered and sorted list of names of packages which match filters""" #TODO: using lower() for case-insensitive comparison doesn't work in some languages @@ -673,39 +692,89 @@ class USERPREF_PT_packages(bpy.types.Panel): contains.append(pkgname) continue - return startswith + contains + return startswith + contains# }}} - def draw_package(metapkg: ConsolidatedPackage, layout: bpy.types.UILayout):# {{{ + def draw_package(metapkg: ConsolidatedPackage, layout: bpy.types.UILayout): #{{{ """Draws the given package""" - def collapsed(): + def draw_operators(metapkg, layout): # {{{ + """ + Draws install, uninstall, update, enable, disable, and preferences + buttons as applicable for the given package + """ + pkg = metapkg.get_display_version() + + if metapkg.installed: + if metapkg.updateable: + layout.operator( + PACKAGE_OT_install.bl_idname, + text="Update to {}".format(utils.fmt_version(metapkg.get_latest_version().version)), + ).package_name=metapkg.name + layout.separator() + + # layout.operator( + # PACKAGE_OT_toggle_preferences.bl_idname, + # text="Preferences", + # ).package_name=metapkg.name + layout.operator( + PACKAGE_OT_uninstall.bl_idname, + text="Uninstall", + ).package_name=metapkg.name + layout.operator( + PACKAGE_OT_toggle_enabled.bl_idname, + text="Disable" if pkg.enabled else "Enable", + ).package_name=metapkg.name + else: + layout.operator( + PACKAGE_OT_install.bl_idname, + text="Install", + ).package_name=metapkg.name + # }}} + + def collapsed(metapkg, layout):# {{{ """Draw collapsed version of package layout""" - lr1 = leftcol.row() - lr2 = leftcol.row() + pkg = metapkg.get_display_version() + + # Only 'install' button is shown when package isn't installed, + # so allow more space for title/description. + spl = layout.split(.5 if pkg.installed else .8) + + metacol = spl.column(align=True) + + buttonrow = spl.row(align=True) + buttonrow.scale_y = 1.5 + buttonrow.alignment = 'RIGHT' + + l1 = metacol.row() + l2 = metacol.row() + + draw_operators(metapkg, buttonrow) + + if pkg.installed and not pkg.enabled: + metacol.enabled = False if pkg.name: - lr1.label(text=pkg.name) + l1.label(text=pkg.name) if pkg.description: - lr2.label(text=pkg.description) - lr2.enabled = False #Give name more visual weight + l2.label(text=pkg.description) + l2.enabled = False #Give name more visual weight + # }}} - def expanded(): + def expanded(metapkg, layout, layoutbox):# {{{ """Draw expanded version of package layout""" - def fmt_version(version_number: tuple) -> str: - """Take version number as a tuple and format it as a string""" - vstr = str(version_number[0]) - for component in version_number[1:]: - vstr += "." + str(component) - return vstr - row1 = leftcol.row() + pkg = metapkg.get_display_version() + + metacol = layoutbox.column(align=True) + row1 = layout.row(align=True) row1.label(pkg.name) + metacol.enabled = row1.enabled = pkg.enabled + if pkg.description: - row2 = leftcol.row() - row2.label(pkg.description) - # row2.scale_y = 1.2 + row = metacol.row() + row.label(pkg.description) def draw_metadatum(label: str, value: str, layout: bpy.types.UILayout): """Draw the given key value pair in a new row in given layout container""" @@ -717,31 +786,37 @@ class USERPREF_PT_packages(bpy.types.Panel): # don't compare against None here; we don't want to display empty arrays/strings either if pkg.location: - draw_metadatum("Location", pkg.location, leftcol) + draw_metadatum("Location", pkg.location, metacol) if pkg.version: - draw_metadatum("Version", fmt_version(pkg.version), leftcol) - # if pkg.blender: - # draw_metadatum("Compatible blender version", fmt_version(pkg.blender), leftcol) + draw_metadatum("Version", utils.fmt_version(pkg.version), metacol) + if pkg.blender: + draw_metadatum("Blender version", utils.fmt_version(pkg.blender), metacol) if pkg.category: - draw_metadatum("Category", pkg.category, leftcol) + draw_metadatum("Category", pkg.category, metacol) if pkg.author: - draw_metadatum("Author", pkg.author, leftcol) + draw_metadatum("Author", pkg.author, metacol) if pkg.support: - draw_metadatum("Support level", pkg.support.title(), leftcol) + draw_metadatum("Support level", pkg.support.title(), metacol) if pkg.warning: - draw_metadatum("Warning", pkg.warning, leftcol) + draw_metadatum("Warning", pkg.warning, metacol) - if pkg.wiki_url or pkg.tracker_url: - padrow = leftcol.row() - padrow.label() - padrow.scale_y = .5 - urlrow = leftcol.row() - urlrow.alignment = 'LEFT' + metacol.separator() + + spl = layoutbox.row().split(.35) + urlrow = spl.row() + urlrow.scale_y = 1.3 + buttonrow = spl.row(align=True) + + urlrow.alignment = 'LEFT' if pkg.wiki_url: urlrow.operator("wm.url_open", text="Documentation", icon='HELP').url=pkg.wiki_url if pkg.tracker_url: urlrow.operator("wm.url_open", text="Report a Bug", icon='URL').url=pkg.tracker_url + buttonrow.alignment = 'RIGHT' + buttonrow.scale_y = 1.3 + draw_operators(metapkg, buttonrow) + def draw_version(layout: bpy.types.UILayout, pkg: Package): """Draw version of package""" spl = layout.split(.9) @@ -749,7 +824,7 @@ class USERPREF_PT_packages(bpy.types.Panel): right = spl.column() right.alignment = 'RIGHT' - left.label(text=fmt_version(pkg.version)) + left.label(text=utils.fmt_version(pkg.version)) if pkg.repository is not None: draw_metadatum("Repository", pkg.repository, left) @@ -761,64 +836,27 @@ class USERPREF_PT_packages(bpy.types.Panel): if len(metapkg.versions) > 1: row = pkgbox.row() - row.label(text="There are multiple providers of this package:") + row.label(text="There are multiple versions of this package:") for version in metapkg.versions: - # row = pkgbox.row() subvbox = pkgbox.box() draw_version(subvbox, version) + # }}} - pkg = metapkg.get_latest_version() - is_expanded = (pkg.name in self.expanded_packages) + is_expanded = (metapkg.name in self.expanded_packages) pkgbox = layout.box() - spl = pkgbox.split(.8) - left = spl.row(align=True) - - # for install/uninstall buttons - right = spl.row() - right.alignment = 'RIGHT' - right.scale_y = 1.5 - - left.operator( + row = pkgbox.row(align=True) + row.operator( WM_OT_package_toggle_expand.bl_idname, icon='TRIA_DOWN' if is_expanded else 'TRIA_RIGHT', emboss=False, - ).package_name=pkg.name - - # for metadata - leftcol = left.column(align=True) - - if pkg.installed: - if pkg.url and pkg.user: - right.operator(PACKAGE_OT_uninstall.bl_idname, - text="Uninstall" - ).package_name=pkg.name - elif pkg.user: - right.label("Installed, but not in repo") - right.scale_y = 2 - right.enabled = False - elif not pkg.user: - l = right.label("System package") - right.scale_y = 2 - right.enabled = False - else: - if pkg.url: - if metapkg.get_latest_installed_version(): - if metapkg.get_latest_installed_version().version < metapkg.get_latest_version().version: - right.operator(PACKAGE_OT_install.bl_idname, - text="Update" - ).package_name=pkg.name - else: - right.operator(PACKAGE_OT_install.bl_idname, - text="Install" - ).package_name=pkg.name - else: - right.label("Not installed, but no URL?") + ).package_name=metapkg.name if is_expanded: - expanded() + expanded(metapkg, row, pkgbox) else: - collapsed()# }}} + collapsed(metapkg, row)# }}} + def center_message(layout, msg: str): """draw a label in the center of an extra-tall row""" @@ -877,6 +915,71 @@ class WM_OT_package_toggle_expand(bpy.types.Operator): return {'FINISHED'} +class PACKAGE_OT_toggle_enabled(bpy.types.Operator): + bl_idname = "package.toggle_enabled" + bl_label = "" + bl_description = "Enable given package if it's disabled, and vice versa if it's enabled" + + log = logging.getLogger(__name__ + ".PACKAGE_OT_toggle_enabled") + + package_name = bpy.props.StringProperty( + name="Package Name", + description="Name of package to enable", + ) + + def execute(self, context): + import addon_utils + global _packages + metapkg = _packages[self.package_name] + + + if not metapkg.installed: + self.report({'ERROR'}, "Can't enable package which isn't installed") + return {'CANCELLED'} + + # enable/disable all installed versions, just in case there are more than one + for pkg in metapkg.versions: + if not pkg.installed: + continue + if not pkg.module_name: + self.log.warning("Can't enable package `%s` without a module name", pkg.name) + continue + + if pkg.enabled: + addon_utils.disable(pkg.module_name, default_set=True) + pkg.enabled = False + self.log.debug("Disabling") + else: + self.log.debug("Enabling") + addon_utils.enable(pkg.module_name, default_set=True) + pkg.enabled = True + + return {'FINISHED'} + +class PACKAGE_OT_disable(bpy.types.Operator): + bl_idname = "package.disable" + bl_label = "" + bl_description = "Disable given package" + + log = logging.getLogger(__name__ + ".PACKAGE_OT_disable") + + package_name = bpy.props.StringProperty( + name="Package Name", + description="Name of package to disable", + ) + + def execute(self, context): + global _packages + package = _packages[self.package_name].get_display_version() + + if not package.module_name: + self.log.error("Can't disable package without a module name") + return {'CANCELLED'} + + ret = bpy.ops.wm.addon_disable(package.module_name) + if ret == {'FINISHED'}: + _packages[self.package_name].enabled = False + return ret class PackageManagerPreferences(bpy.types.AddonPreferences): bl_idname = __package__ @@ -938,6 +1041,13 @@ def build_composite_packagelist(installed: list, available: list) -> OrderedDict return in_user or in_prefs + def is_enabled(pkg: Package) -> bool: + """Check if package is enabled""" + if pkg.module_name is not None: + return (pkg.module_name in bpy.context.user_preferences.addons) + else: + raise ValueError("Can't determine if package is enabled without knowing its module name") + for pkg in available: pkgname = pkg.bl_info['name'] @@ -948,6 +1058,8 @@ def build_composite_packagelist(installed: list, available: list) -> OrderedDict for pkg in installed: pkg.installed = True + if pkg.name == 'Node Wrangler': + log.debug("node wrangler %s", pkg.module_name) pkg.user = is_user_package(pkg) if pkg.name in masterlist: for masterpkg in masterlist[pkg.name]: @@ -955,17 +1067,24 @@ def build_composite_packagelist(installed: list, available: list) -> OrderedDict masterpkg.installed = True masterpkg.installed_location = pkg.installed_location masterpkg.user = pkg.user + masterpkg.module_name = pkg.module_name break else: + if masterpkg.version > pkg.version: + masterlist[pkg.name].updateable = True masterlist[pkg.name].add_version(pkg) else: masterlist[pkg.name] = ConsolidatedPackage(pkg) + masterlist[pkg.name].enabled = is_enabled(pkg) + return OrderedDict(sorted(masterlist.items())) def register(): bpy.utils.register_class(PACKAGE_OT_install) bpy.utils.register_class(PACKAGE_OT_uninstall) + bpy.utils.register_class(PACKAGE_OT_toggle_enabled) + # bpy.utils.register_class(PACKAGE_OT_disable) bpy.utils.register_class(PACKAGE_OT_refresh_repositories) bpy.utils.register_class(PACKAGE_OT_refresh_packages) bpy.utils.register_class(PACKAGE_OT_refresh) @@ -996,6 +1115,8 @@ def register(): def unregister(): bpy.utils.unregister_class(PACKAGE_OT_install) bpy.utils.unregister_class(PACKAGE_OT_uninstall) + bpy.utils.unregister_class(PACKAGE_OT_toggle_enabled) + # bpy.utils.unregister_class(PACKAGE_OT_disable) bpy.utils.unregister_class(PACKAGE_OT_refresh_repositories) bpy.utils.unregister_class(PACKAGE_OT_refresh_packages) bpy.utils.unregister_class(PACKAGE_OT_refresh) diff --git a/package_manager/bpkg/types.py b/package_manager/bpkg/types.py index 7fd3dcd..e3068c4 100644 --- a/package_manager/bpkg/types.py +++ b/package_manager/bpkg/types.py @@ -18,8 +18,10 @@ class Package: self.set_from_dict(package_dict) self.installed = False + self.enabled = False self.repository = None self.installed_location = None + self.module_name = None def to_dict(self) -> dict: """ @@ -162,6 +164,7 @@ class Package: pkg = cls() pkg.files = [filepath.name] pkg.installed_location = str(filepath) + pkg.module_name = module.__name__ try: pkg.bl_info = module.bl_info diff --git a/package_manager/utils.py b/package_manager/utils.py index 4884b66..7637bdc 100644 --- a/package_manager/utils.py +++ b/package_manager/utils.py @@ -1,5 +1,12 @@ from pathlib import Path +def fmt_version(version_number: tuple) -> str: + """Take version number as a tuple and format it as a string""" + vstr = str(version_number[0]) + for component in version_number[1:]: + vstr += "." + str(component) + return vstr + def parse_repository_url(url: str) -> str: """Sanitize repository url""" from urllib.parse import urlsplit, urlunsplit