Extensions: improve extensions UI for URL & filepath dropping

- Add cancel button.
- Add repository selector when installing from files.
- Make enabling after installing optional.

Implement design from #118635.
This commit is contained in:
Campbell Barton
2024-02-23 19:21:07 +11:00
parent b3b459e482
commit ff451df8e1
3 changed files with 190 additions and 54 deletions

View File

@ -180,20 +180,13 @@ def extenion_repos_upgrade(*_):
def extenion_url_drop(url):
from .bl_extension_ui import (
extension_drop_url_popover,
extension_drop_filepath_popover,
extension_drop_file_popover,
)
wm = bpy.context.window_manager
if url.startswith(("http://", "https://", "file://")):
wm.popover(
lambda panel, context: extension_drop_url_popover(panel, context, url),
ui_units_x=14,
)
extension_drop_url_popover(url)
else:
wm.popover(
lambda panel, context: extension_drop_filepath_popover(panel, context, url),
ui_units_x=14,
)
extension_drop_file_popover(url)
# -----------------------------------------------------------------------------
@ -411,6 +404,19 @@ def register():
default=True,
)
# Use for drag & drop operators.
from .bl_extension_ops import rna_prop_repo_enum_local_only_itemf
WindowManager.extension_local_repos = EnumProperty(
name="Local Repository",
description="Local repository",
items=rna_prop_repo_enum_local_only_itemf,
)
WindowManager.extension_enable_on_install = BoolProperty(
name="Enable Add-on",
description="Enable on install",
default=True,
)
from bl_ui.space_userpref import USERPREF_MT_interface_theme_presets
USERPREF_MT_interface_theme_presets.append(theme_preset_draw)

View File

@ -50,10 +50,6 @@ from .bl_extension_utils import (
RepoLockContext,
)
# Enable extensions when they're installed.
USE_ENABLE_ON_INSTALL = True
rna_prop_url = StringProperty(name="URL", subtype='FILE_PATH', options={'HIDDEN'})
rna_prop_directory = StringProperty(name="Repo Directory", subtype='FILE_PATH')
rna_prop_repo_index = IntProperty(name="Repo Index", default=-1)
@ -126,6 +122,13 @@ class CheckSIGINT_Context:
# Internal Utilities
#
def wm_close_popup_hack():
# Setting the workspace closes popup, we may want an API for this.
from bpy import context
window = context.window
window.workspace = window.workspace
def online_user_agent_from_blender():
# NOTE: keep this brief and avoid `platform.platform()` which could identify individual users.
# Produces something like this: `Blender/4.2.0 (Linux x86_64; cycle=alpha)` or similar.
@ -1095,10 +1098,20 @@ class BlPkgPkgInstallFiles(Operator, _BlPkgCmdMixIn):
description="The local repository to install extensions into",
)
enable_on_install: BoolProperty(
name="Enable Add-on",
description="Enable add-ons after installing",
default=True,
)
def exec_command_iter(self, is_modal):
from .bl_extension_utils import (
pkg_manifest_dict_from_file_or_error,
)
from .bl_extension_ui import (
extension_drop_file_popover_close_as_needed,
)
extension_drop_file_popover_close_as_needed()
self._addon_restore = []
@ -1223,7 +1236,7 @@ class BlPkgPkgInstallFiles(Operator, _BlPkgCmdMixIn):
pkg_id_sequence_installed = pkg_id_sequence
# Install.
if USE_ENABLE_ON_INSTALL:
if self.enable_on_install:
import addon_utils
# TODO: it would be nice to include this message in the banner.
@ -1272,7 +1285,18 @@ class BlPkgPkgInstall(Operator, _BlPkgCmdMixIn):
pkg_id: rna_prop_pkg_id
enable_on_install: BoolProperty(
name="Enable Add-on",
description="Enable add-ons after installing",
default=True,
)
def exec_command_iter(self, is_modal):
from .bl_extension_ui import (
extension_drop_url_popover_close_as_needed,
)
extension_drop_url_popover_close_as_needed()
self._addon_restore = []
directory = _repo_dir_and_index_get(self.repo_index, self.repo_directory, self.report)
@ -1352,7 +1376,7 @@ class BlPkgPkgInstall(Operator, _BlPkgCmdMixIn):
)
else:
# Install.
if USE_ENABLE_ON_INSTALL:
if self.enable_on_install:
import addon_utils
# TODO: it would be nice to include this message in the banner.
@ -1678,6 +1702,16 @@ class BlPkgEnableNotInstalled(Operator):
return {'CANCELLED'}
class BlPkgPopupCancel(Operator):
"""Close the popup"""
bl_idname = "bl_pkg.popup_cancel"
bl_label = "Cancel"
def execute(self, context):
wm_close_popup_hack()
return {'CANCELLED'}
# -----------------------------------------------------------------------------
# Register
#
@ -1709,6 +1743,7 @@ classes = (
# Dummy, just shows a message.
BlPkgEnableNotInstalled,
BlPkgPopupCancel,
# Dummy commands (for testing).
BlPkgDummyProgress,

View File

@ -9,8 +9,10 @@ Written to allow a UI without modifying Blender.
__all__ = (
"display_errors",
"extension_drop_filepath_popover",
"extension_drop_file_popover",
"extension_drop_file_popover_close_as_needed",
"extension_drop_url_popover",
"extension_drop_url_popover_close_as_needed",
"register",
"unregister",
)
@ -113,55 +115,57 @@ def extension_url_find_repo_index_and_pkg_id(url):
return -1, "", "", None, None
def extension_drop_url_popover(panel, context, url):
layout = panel.layout
def wm_error(title, body, *, icon):
def fn(panel, context):
layout = panel.layout
layout.label(text=title, icon=icon)
if body:
layout.label(text=body)
wm = bpy.context.window_manager
wm.popover(fn, ui_units_x=14)
def extension_drop_url_popover(url):
repo_index, repo_name, pkg_id, item_remote, item_local = extension_url_find_repo_index_and_pkg_id(url)
if repo_index == -1:
layout.label(text="Extension: URL not found in remote repositories!", icon='ERROR')
layout.label(text=url)
wm_error("Extension: URL not found in remote repositories!", url, icon='ERROR')
return
if item_local is not None:
layout.label(text="Extension: {:s}".format(pkg_id), icon='INFO')
layout.label(text="Already installed!")
wm_error("Extension: {:s}".format(pkg_id), "Already installed!", icon='ERROR')
return
layout.label(text="Install Extension")
layout.separator(type='LINE')
layout.label(text="Do you want to install the following {:s}?".format(item_remote["type"]))
col = layout.column(align=True)
col.label(text="Name: {:s}".format(item_remote["name"]))
col.label(text="Repository: {:s}".format(repo_name))
col.label(text="Size: {:s}".format(size_as_fmt_string(item_remote["archive_size"], precision=0)))
del col
layout.separator()
# if item_remote["type"] == "add-on":
# # TODO: option to enable.
# pass
row = layout.row()
props = row.operator("bl_pkg.pkg_install", text="Install & Enable")
props.repo_index = repo_index
props.pkg_id = pkg_id
USERPREF_PT_extensions_bl_pkg_drop_url.drop_variables = repo_index, repo_name, pkg_id, item_remote
bpy.ops.wm.call_panel(name="USERPREF_PT_extensions_bl_pkg_drop_url", keep_open=True)
def extension_drop_filepath_popover(panel, context, url):
layout = panel.layout
def extension_drop_url_popover_close_as_needed():
if USERPREF_PT_extensions_bl_pkg_drop_url.drop_variables is None:
return
USERPREF_PT_extensions_bl_pkg_drop_url.drop_variables = None
from .bl_extension_ops import wm_close_popup_hack
wm_close_popup_hack()
# TODO: this UI isn't so nice, ideally the repo can be selected with an OK button.
# This is more complicated than it might seem as calling `bpy.ops.*` doesn't forward errors to the window-manager.
# The API may need to be extended to better support this use-case.
layout.operator_context = 'EXEC_DEFAULT'
layout.label(text="Install from Disk")
layout.separator(type='LINE')
props = layout.operator_menu_enum("bl_pkg.pkg_install_files", "repo", text="Local Repository")
props.filepath = url
def extension_drop_file_popover(url):
from .bl_extension_ops import repo_iter_valid_local_only
if not list(repo_iter_valid_local_only(bpy.context)):
wm_error("Error", "No Local Repositories", icon='ERROR')
return
USERPREF_PT_extensions_bl_pkg_drop_file.drop_variables = url
bpy.ops.wm.call_panel(name="USERPREF_PT_extensions_bl_pkg_drop_file", keep_open=True)
def extension_drop_file_popover_close_as_needed():
if USERPREF_PT_extensions_bl_pkg_drop_file.drop_variables is None:
return
USERPREF_PT_extensions_bl_pkg_drop_file.drop_variables = None
from .bl_extension_ops import wm_close_popup_hack
wm_close_popup_hack()
# -----------------------------------------------------------------------------
@ -794,10 +798,101 @@ def extensions_panel_draw(panel, context):
)
class USERPREF_PT_extensions_bl_pkg_drop_url(Panel):
bl_label = "Drop URL"
bl_space_type = 'TOPBAR' # dummy.
bl_region_type = 'HEADER'
bl_ui_units_x = 13
# WARNING: workaround for not being able to pass arguments to a popup.
drop_variables = None
def draw(self, context):
layout = self.layout
repo_index, repo_name, pkg_id, item_remote = USERPREF_PT_extensions_bl_pkg_drop_url.drop_variables
layout.label(text="Install Extension")
layout.separator(type='LINE')
layout.label(text="Do you want to install the following {:s}?".format(item_remote["type"]))
col = layout.column(align=True)
col.label(text="Name: {:s}".format(item_remote["name"]))
col.label(text="Repository: {:s}".format(repo_name))
col.label(text="Size: {:s}".format(size_as_fmt_string(item_remote["archive_size"], precision=0)))
del col
layout.separator()
if item_remote["type"] == "add-on":
wm = context.window_manager
layout.prop(wm, "extension_enable_on_install")
enable_on_install = wm.extension_enable_on_install
else:
enable_on_install = False
layout.separator()
row = layout.row()
row.operator("bl_pkg.popup_cancel", text="Cancel")
props = row.operator("bl_pkg.pkg_install", text="Install")
props.repo_index = repo_index
props.pkg_id = pkg_id
props.enable_on_install = enable_on_install
class USERPREF_PT_extensions_bl_pkg_drop_file(Panel):
bl_label = "Drop File"
bl_space_type = 'TOPBAR' # dummy.
bl_region_type = 'HEADER'
bl_ui_units_x = 13
# WARNING: workaround for not being able to pass arguments to a popup.
drop_variables = None
def draw(self, context):
layout = self.layout
url = USERPREF_PT_extensions_bl_pkg_drop_file.drop_variables
# TODO: this UI isn't so nice, ideally the repo can be selected with an OK button.
# This is more complicated than it might seem as calling `bpy.ops.*` doesn't forward errors to the window-manager.
# The API may need to be extended to better support this use-case.
layout.operator_context = 'EXEC_DEFAULT'
layout.label(text="Install from Disk")
layout.separator(type='LINE')
wm = context.window_manager
layout.label(text="Local Repository")
layout.prop(wm, "extension_local_repos", text="")
# TODO: inspect the ZIP and find if the type is an add-on.
if True:
wm = context.window_manager
layout.prop(wm, "extension_enable_on_install")
enable_on_install = wm.extension_enable_on_install
row = layout.row()
row.operator("bl_pkg.popup_cancel", text="Cancel")
props = row.operator("bl_pkg.pkg_install_files", text="Install")
props.repo = wm.extension_local_repos
props.filepath = url
props.enable_on_install = enable_on_install
classes = (
# Pop-overs.
USERPREF_PT_extensions_bl_pkg_filter,
USERPREF_MT_extensions_bl_pkg_settings,
USERPREF_PT_extensions_bl_pkg_drop_url,
USERPREF_PT_extensions_bl_pkg_drop_file,
)