diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 3568846..dd9d9a8 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -3,7 +3,7 @@ from enum import Enum, auto from functools import partial from pathlib import Path -from typing import Dict, List, Literal, Optional, Sequence, Tuple +from typing import Dict, List, Literal, NamedTuple, Optional, Sequence, Tuple import napari.plugins import napari.resources @@ -47,19 +47,26 @@ InstallerQueue, InstallerTools, ) +from napari_plugin_manager.qt_widgets import ClickableLabel from napari_plugin_manager.utils import is_conda_package # TODO: add error icon and handle pip install errors # Scaling factor for each list widget item when expanding. SCALE = 1.6 - CONDA = 'Conda' PYPI = 'PyPI' ON_BUNDLE = running_as_constructor_app() IS_NAPARI_CONDA_INSTALLED = is_conda_package('napari') +class ProjectInfoVersions(NamedTuple): + metadata: npe2.PackageMetadata + display_name: str + pypi_versions: List[str] + conda_versions: List[str] + + class PluginListItem(QFrame): """An entry in the plugin dialog. This will include the package name, summary, author, source, version, and buttons to update, install/uninstall, etc.""" @@ -67,6 +74,7 @@ class PluginListItem(QFrame): def __init__( self, package_name: str, + display_name: str, version: str = '', url: str = '', summary: str = '', @@ -86,7 +94,12 @@ def __init__( self._versions_conda = versions_conda self._versions_pypi = versions_pypi self.setup_ui(enabled) - self.plugin_name.setText(package_name) + if package_name == display_name: + name = package_name + else: + name = f"{display_name} ({package_name})" + + self.plugin_name.setText(name) if len(versions_pypi) > 0: self._populate_version_dropdown(PYPI) @@ -188,7 +201,7 @@ def setup_ui(self, enabled=True): self.enabled_checkbox.setMinimumSize(QSize(20, 0)) self.enabled_checkbox.setText("") self.row1.addWidget(self.enabled_checkbox) - self.plugin_name = QPushButton(self) + self.plugin_name = ClickableLabel(self) # To style content # Do not want to highlight on hover unless there is a website. if self.url and self.url != 'UNKNOWN': self.plugin_name.setObjectName('plugin_name_web') @@ -436,17 +449,13 @@ def _count_visible(self) -> int: @Slot(tuple) def addItem( self, - project_info_versions: Tuple[ - npe2.PackageMetadata, List[str], List[str] - ], + project_info: ProjectInfoVersions, installed=False, plugin_name=None, enabled=True, npe_version=None, ): - project_info, versions_pypi, versions_conda = project_info_versions - - pkg_name = project_info.name + pkg_name = project_info.metadata.name # don't add duplicates if ( self.findItems(pkg_name, Qt.MatchFlag.MatchFixedString) @@ -455,24 +464,25 @@ def addItem( return # including summary here for sake of filtering below. - searchable_text = f"{pkg_name} {project_info.summary}" + searchable_text = f"{pkg_name} {project_info.display_name} {project_info.metadata.summary}" item = QListWidgetItem(searchable_text, self) - item.version = project_info.version + item.version = project_info.metadata.version super().addItem(item) widg = PluginListItem( package_name=pkg_name, - version=project_info.version, - url=project_info.home_page, - summary=project_info.summary, - author=project_info.author, - license=project_info.license, + display_name=project_info.display_name, + version=project_info.metadata.version, + url=project_info.metadata.home_page, + summary=project_info.metadata.summary, + author=project_info.metadata.author, + license=project_info.metadata.license, parent=self, plugin_name=plugin_name, enabled=enabled, installed=installed, npe_version=npe_version, - versions_conda=versions_conda, - versions_pypi=versions_pypi, + versions_conda=project_info.conda_versions, + versions_pypi=project_info.pypi_versions, ) item.widget = widg item.npe_version = npe_version @@ -480,12 +490,12 @@ def addItem( item.setSizeHint(widg.sizeHint()) self.setItemWidget(item, widg) - if project_info.home_page: + if project_info.metadata.home_page: import webbrowser # FIXME: Partial may lead to leak memory when connecting to Qt signals. widg.plugin_name.clicked.connect( - partial(webbrowser.open, project_info.home_page) + partial(webbrowser.open, project_info.metadata.home_page) ) # FIXME: Partial may lead to leak memory when connecting to Qt signals. @@ -620,9 +630,7 @@ def handle_action( widget.setProperty("current_job_id", None) @Slot(npe2.PackageMetadata, bool) - def tag_outdated( - self, project_info: npe2.PackageMetadata, is_available: bool - ): + def tag_outdated(self, metadata: npe2.PackageMetadata, is_available: bool): """Determines if an installed plugin is up to date with the latest version. If it is not, the latest version will be displayed on the update button. """ @@ -630,10 +638,10 @@ def tag_outdated( return for item in self.findItems( - project_info.name, Qt.MatchFlag.MatchStartsWith + metadata.name, Qt.MatchFlag.MatchStartsWith ): current = item.version - latest = project_info.version + latest = metadata.version if parse_version(current) >= parse_version(latest): continue if hasattr(item, 'outdated'): @@ -648,7 +656,7 @@ def tag_outdated( trans._("update (v{latest})", latest=latest) ) - def tag_unavailable(self, project_info: npe2.PackageMetadata): + def tag_unavailable(self, metadata: npe2.PackageMetadata): """ Tag list items as unavailable for install with conda-forge. @@ -656,7 +664,7 @@ def tag_unavailable(self, project_info: npe2.PackageMetadata): icon with a hover tooltip. """ for item in self.findItems( - project_info.name, Qt.MatchFlag.MatchStartsWith + metadata.name, Qt.MatchFlag.MatchStartsWith ): widget = self.itemWidget(item) widget.show_warning( @@ -771,7 +779,7 @@ def _add_to_installed(distname, enabled, npe_version=1): meta = {} self.installed_list.addItem( - ( + ProjectInfoVersions( npe2.PackageMetadata( metadata_version="1.0", name=norm_name, @@ -781,6 +789,7 @@ def _add_to_installed(distname, enabled, npe_version=1): author=meta.get('author', ''), license=meta.get('license', ''), ), + norm_name, [], [], ), @@ -1019,23 +1028,23 @@ def _add_items(self): return data = self._plugin_data.pop(0) - project_info, is_available_in_conda, extra_info = data - if project_info.name in self.already_installed: - self.installed_list.tag_outdated( - project_info, is_available_in_conda - ) + metadata, is_available_in_conda, extra_info = data + display_name = extra_info.get('display_name', metadata.name) + if metadata.name in self.already_installed: + self.installed_list.tag_outdated(metadata, is_available_in_conda) else: - if project_info.name not in self.available_set: - self.available_set.add(project_info.name) + if metadata.name not in self.available_set: + self.available_set.add(metadata.name) self.available_list.addItem( - ( - project_info, + ProjectInfoVersions( + metadata, + display_name, extra_info['pypi_versions'], extra_info['conda_versions'], ) ) if ON_BUNDLE and not is_available_in_conda: - self.available_list.tag_unavailable(project_info) + self.available_list.tag_unavailable(metadata) self.filter() diff --git a/napari_plugin_manager/qt_widgets.py b/napari_plugin_manager/qt_widgets.py new file mode 100644 index 0000000..f115071 --- /dev/null +++ b/napari_plugin_manager/qt_widgets.py @@ -0,0 +1,14 @@ +from qtpy.QtCore import Signal +from qtpy.QtGui import QMouseEvent +from qtpy.QtWidgets import QLabel + + +class ClickableLabel(QLabel): + clicked = Signal() + + def __init__(self, parent=None): + super().__init__(parent=parent) + + def mouseReleaseEvent(self, event: QMouseEvent): + super().mouseReleaseEvent(event) + self.clicked.emit()