napari-plugin-manager 0.1.0a2__py3-none-any.whl → 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napari_plugin_manager/_tests/conftest.py +45 -0
- napari_plugin_manager/_tests/test_installer_process.py +179 -67
- napari_plugin_manager/_tests/test_npe2api.py +54 -0
- napari_plugin_manager/_tests/test_qt_plugin_dialog.py +269 -60
- napari_plugin_manager/_tests/test_utils.py +27 -0
- napari_plugin_manager/_version.py +14 -2
- napari_plugin_manager/npe2api.py +131 -0
- napari_plugin_manager/qt_package_installer.py +166 -60
- napari_plugin_manager/qt_plugin_dialog.py +871 -407
- napari_plugin_manager/qt_widgets.py +14 -0
- napari_plugin_manager/styles.qss +383 -0
- napari_plugin_manager/utils.py +22 -0
- napari_plugin_manager-0.1.2.dist-info/METADATA +257 -0
- napari_plugin_manager-0.1.2.dist-info/RECORD +19 -0
- {napari_plugin_manager-0.1.0a2.dist-info → napari_plugin_manager-0.1.2.dist-info}/WHEEL +1 -1
- napari_plugin_manager-0.1.0a2.dist-info/METADATA +0 -107
- napari_plugin_manager-0.1.0a2.dist-info/RECORD +0 -13
- {napari_plugin_manager-0.1.0a2.dist-info → napari_plugin_manager-0.1.2.dist-info}/LICENSE +0 -0
- {napari_plugin_manager-0.1.0a2.dist-info → napari_plugin_manager-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,29 +1,37 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import importlib.metadata
|
|
2
3
|
import os
|
|
3
|
-
import re
|
|
4
4
|
import sys
|
|
5
|
-
|
|
5
|
+
import webbrowser
|
|
6
6
|
from functools import partial
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Dict, List, Literal, Optional, Sequence, Tuple
|
|
8
|
+
from typing import Dict, List, Literal, NamedTuple, Optional, Sequence, Tuple
|
|
9
9
|
|
|
10
10
|
import napari.plugins
|
|
11
11
|
import napari.resources
|
|
12
12
|
import npe2
|
|
13
|
-
from napari._qt.qt_resources import QColoredSVGIcon
|
|
13
|
+
from napari._qt.qt_resources import QColoredSVGIcon, get_current_stylesheet
|
|
14
14
|
from napari._qt.qthreading import create_worker
|
|
15
15
|
from napari._qt.widgets.qt_message_popup import WarnPopup
|
|
16
16
|
from napari._qt.widgets.qt_tooltip import QtToolTipLabel
|
|
17
|
-
from napari.plugins.npe2api import iter_napari_plugin_info
|
|
18
17
|
from napari.plugins.utils import normalized_name
|
|
19
18
|
from napari.settings import get_settings
|
|
20
19
|
from napari.utils.misc import (
|
|
20
|
+
StringEnum,
|
|
21
21
|
parse_version,
|
|
22
22
|
running_as_constructor_app,
|
|
23
23
|
)
|
|
24
|
+
from napari.utils.notifications import show_info, show_warning
|
|
24
25
|
from napari.utils.translations import trans
|
|
25
|
-
from qtpy.QtCore import
|
|
26
|
-
from qtpy.QtGui import
|
|
26
|
+
from qtpy.QtCore import QPoint, QSize, Qt, QTimer, Signal, Slot
|
|
27
|
+
from qtpy.QtGui import (
|
|
28
|
+
QAction,
|
|
29
|
+
QActionGroup,
|
|
30
|
+
QFont,
|
|
31
|
+
QKeySequence,
|
|
32
|
+
QMovie,
|
|
33
|
+
QShortcut,
|
|
34
|
+
)
|
|
27
35
|
from qtpy.QtWidgets import (
|
|
28
36
|
QCheckBox,
|
|
29
37
|
QComboBox,
|
|
@@ -35,95 +43,136 @@ from qtpy.QtWidgets import (
|
|
|
35
43
|
QLineEdit,
|
|
36
44
|
QListWidget,
|
|
37
45
|
QListWidgetItem,
|
|
46
|
+
QMenu,
|
|
38
47
|
QPushButton,
|
|
39
48
|
QSizePolicy,
|
|
40
49
|
QSplitter,
|
|
41
50
|
QTextEdit,
|
|
51
|
+
QToolButton,
|
|
42
52
|
QVBoxLayout,
|
|
43
53
|
QWidget,
|
|
44
54
|
)
|
|
45
55
|
from superqt import QCollapsible, QElidingLabel
|
|
46
56
|
|
|
57
|
+
from napari_plugin_manager.npe2api import (
|
|
58
|
+
cache_clear,
|
|
59
|
+
iter_napari_plugin_info,
|
|
60
|
+
)
|
|
47
61
|
from napari_plugin_manager.qt_package_installer import (
|
|
48
62
|
InstallerActions,
|
|
49
63
|
InstallerQueue,
|
|
50
64
|
InstallerTools,
|
|
65
|
+
ProcessFinishedData,
|
|
51
66
|
)
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
from napari_plugin_manager.qt_widgets import ClickableLabel
|
|
68
|
+
from napari_plugin_manager.utils import is_conda_package
|
|
54
69
|
|
|
55
70
|
# Scaling factor for each list widget item when expanding.
|
|
56
|
-
SCALE = 1.6
|
|
57
|
-
|
|
58
71
|
CONDA = 'Conda'
|
|
59
72
|
PYPI = 'PyPI'
|
|
73
|
+
ON_BUNDLE = running_as_constructor_app()
|
|
74
|
+
IS_NAPARI_CONDA_INSTALLED = is_conda_package('napari')
|
|
75
|
+
STYLES_PATH = Path(__file__).parent / 'styles.qss'
|
|
60
76
|
|
|
61
77
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
def _show_message(widget):
|
|
79
|
+
message = trans._(
|
|
80
|
+
'When installing/uninstalling npe2 plugins, '
|
|
81
|
+
'you must restart napari for UI changes to take effect.'
|
|
82
|
+
)
|
|
83
|
+
if widget.isVisible():
|
|
84
|
+
button = widget.action_button
|
|
85
|
+
warn_dialog = WarnPopup(text=message)
|
|
86
|
+
global_point = widget.action_button.mapToGlobal(
|
|
87
|
+
button.rect().topRight()
|
|
88
|
+
)
|
|
89
|
+
global_point = QPoint(
|
|
90
|
+
global_point.x() - button.width(), global_point.y()
|
|
91
|
+
)
|
|
92
|
+
warn_dialog.move(global_point)
|
|
93
|
+
warn_dialog.exec_()
|
|
69
94
|
|
|
70
|
-
# Installed conda packages within a conda installation and environment can be identified as files
|
|
71
|
-
# with the template `<package-name>-<version>-<build-string>.json` saved within a `conda-meta` folder within
|
|
72
|
-
# the given environment of interest.
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
class ProjectInfoVersions(NamedTuple):
|
|
97
|
+
metadata: npe2.PackageMetadata
|
|
98
|
+
display_name: str
|
|
99
|
+
pypi_versions: List[str]
|
|
100
|
+
conda_versions: List[str]
|
|
79
101
|
|
|
80
102
|
|
|
81
103
|
class PluginListItem(QFrame):
|
|
82
104
|
"""An entry in the plugin dialog. This will include the package name, summary,
|
|
83
105
|
author, source, version, and buttons to update, install/uninstall, etc."""
|
|
84
106
|
|
|
107
|
+
# item, package_name, action_name, version, installer_choice
|
|
108
|
+
actionRequested = Signal(QListWidgetItem, str, StringEnum, str, StringEnum)
|
|
109
|
+
|
|
85
110
|
def __init__(
|
|
86
111
|
self,
|
|
112
|
+
item: QListWidgetItem,
|
|
87
113
|
package_name: str,
|
|
114
|
+
display_name: str,
|
|
88
115
|
version: str = '',
|
|
89
116
|
url: str = '',
|
|
90
117
|
summary: str = '',
|
|
91
118
|
author: str = '',
|
|
92
119
|
license: str = "UNKNOWN", # noqa: A002
|
|
93
120
|
*,
|
|
94
|
-
plugin_name: str = None,
|
|
121
|
+
plugin_name: Optional[str] = None,
|
|
95
122
|
parent: QWidget = None,
|
|
96
123
|
enabled: bool = True,
|
|
97
124
|
installed: bool = False,
|
|
98
125
|
npe_version=1,
|
|
99
|
-
versions_conda: List[str] = None,
|
|
100
|
-
versions_pypi: List[str] = None,
|
|
126
|
+
versions_conda: Optional[List[str]] = None,
|
|
127
|
+
versions_pypi: Optional[List[str]] = None,
|
|
128
|
+
prefix=None,
|
|
101
129
|
) -> None:
|
|
102
130
|
super().__init__(parent)
|
|
131
|
+
self.prefix = prefix
|
|
132
|
+
self.item = item
|
|
103
133
|
self.url = url
|
|
134
|
+
self.name = package_name
|
|
135
|
+
self.npe_version = npe_version
|
|
136
|
+
self._version = version
|
|
104
137
|
self._versions_conda = versions_conda
|
|
105
138
|
self._versions_pypi = versions_pypi
|
|
106
139
|
self.setup_ui(enabled)
|
|
107
|
-
|
|
140
|
+
|
|
141
|
+
if package_name == display_name:
|
|
142
|
+
name = package_name
|
|
143
|
+
else:
|
|
144
|
+
name = f"{display_name} <small>({package_name})</small>"
|
|
145
|
+
|
|
146
|
+
self.plugin_name.setText(name)
|
|
108
147
|
|
|
109
148
|
if len(versions_pypi) > 0:
|
|
110
149
|
self._populate_version_dropdown(PYPI)
|
|
111
150
|
else:
|
|
112
151
|
self._populate_version_dropdown(CONDA)
|
|
113
152
|
|
|
114
|
-
|
|
153
|
+
mod_version = version.replace('.', '․') # noqa: RUF001
|
|
154
|
+
self.version.setWordWrap(True)
|
|
155
|
+
self.version.setText(mod_version)
|
|
156
|
+
self.version.setToolTip(version)
|
|
157
|
+
|
|
115
158
|
if summary:
|
|
116
|
-
self.summary.setText(summary
|
|
159
|
+
self.summary.setText(summary)
|
|
160
|
+
|
|
117
161
|
if author:
|
|
118
162
|
self.package_author.setText(author)
|
|
163
|
+
|
|
119
164
|
self.package_author.setWordWrap(True)
|
|
120
165
|
self.cancel_btn.setVisible(False)
|
|
121
166
|
|
|
122
167
|
self._handle_npe2_plugin(npe_version)
|
|
168
|
+
self._set_installed(installed, package_name)
|
|
169
|
+
self._populate_version_dropdown(self.get_installer_source())
|
|
123
170
|
|
|
171
|
+
def _set_installed(self, installed: bool, package_name):
|
|
124
172
|
if installed:
|
|
125
173
|
if is_conda_package(package_name):
|
|
126
174
|
self.source.setText(CONDA)
|
|
175
|
+
|
|
127
176
|
self.enabled_checkbox.show()
|
|
128
177
|
self.action_button.setText(trans._("Uninstall"))
|
|
129
178
|
self.action_button.setObjectName("remove_button")
|
|
@@ -136,28 +185,35 @@ class PluginListItem(QFrame):
|
|
|
136
185
|
self.action_button.setObjectName("install_button")
|
|
137
186
|
self.info_widget.hide()
|
|
138
187
|
self.install_info_button.addWidget(self.info_choice_wdg)
|
|
139
|
-
self.install_info_button.setFixedWidth(170)
|
|
140
|
-
|
|
141
188
|
self.info_choice_wdg.show()
|
|
142
189
|
|
|
143
190
|
def _handle_npe2_plugin(self, npe_version):
|
|
144
191
|
if npe_version in (None, 1):
|
|
145
192
|
return
|
|
193
|
+
|
|
146
194
|
opacity = 0.4 if npe_version == 'shim' else 1
|
|
147
|
-
|
|
148
|
-
npe2_icon = QLabel(self)
|
|
195
|
+
text = trans._('npe1 (adapted)') if npe_version == 'shim' else 'npe2'
|
|
149
196
|
icon = QColoredSVGIcon.from_resources('logo_silhouette')
|
|
150
|
-
|
|
151
|
-
icon.colored(color='#33F0FF', opacity=opacity).pixmap(20, 20)
|
|
197
|
+
self.set_status(
|
|
198
|
+
icon.colored(color='#33F0FF', opacity=opacity).pixmap(20, 20), text
|
|
152
199
|
)
|
|
153
|
-
|
|
154
|
-
|
|
200
|
+
|
|
201
|
+
def set_status(self, icon=None, text=''):
|
|
202
|
+
"""Set the status icon and text. next to the package name."""
|
|
203
|
+
if icon:
|
|
204
|
+
self.status_icon.setPixmap(icon)
|
|
205
|
+
|
|
206
|
+
if text:
|
|
207
|
+
self.status_label.setText(text)
|
|
208
|
+
|
|
209
|
+
self.status_icon.setVisible(bool(icon))
|
|
210
|
+
self.status_label.setVisible(bool(text))
|
|
155
211
|
|
|
156
212
|
def set_busy(
|
|
157
213
|
self,
|
|
158
214
|
text: str,
|
|
159
|
-
action_name:
|
|
160
|
-
|
|
215
|
+
action_name: Optional[
|
|
216
|
+
Literal['install', 'uninstall', 'cancel', 'upgrade']
|
|
161
217
|
] = None,
|
|
162
218
|
):
|
|
163
219
|
"""Updates status text and what buttons are visible when any button is pushed.
|
|
@@ -186,16 +242,13 @@ class PluginListItem(QFrame):
|
|
|
186
242
|
|
|
187
243
|
def setup_ui(self, enabled=True):
|
|
188
244
|
"""Define the layout of the PluginListItem"""
|
|
189
|
-
|
|
190
|
-
self.v_lay = QVBoxLayout(self)
|
|
191
|
-
self.v_lay.setContentsMargins(-1, 6, -1, 6)
|
|
192
|
-
self.v_lay.setSpacing(0)
|
|
193
|
-
self.row1 = QHBoxLayout()
|
|
194
|
-
self.row1.setSpacing(6)
|
|
245
|
+
# Enabled checkbox
|
|
195
246
|
self.enabled_checkbox = QCheckBox(self)
|
|
196
247
|
self.enabled_checkbox.setChecked(enabled)
|
|
197
|
-
self.enabled_checkbox.stateChanged.connect(self._on_enabled_checkbox)
|
|
198
248
|
self.enabled_checkbox.setToolTip(trans._("enable/disable"))
|
|
249
|
+
self.enabled_checkbox.setText("")
|
|
250
|
+
self.enabled_checkbox.stateChanged.connect(self._on_enabled_checkbox)
|
|
251
|
+
|
|
199
252
|
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
200
253
|
sizePolicy.setHorizontalStretch(0)
|
|
201
254
|
sizePolicy.setVerticalStretch(0)
|
|
@@ -204,11 +257,22 @@ class PluginListItem(QFrame):
|
|
|
204
257
|
)
|
|
205
258
|
self.enabled_checkbox.setSizePolicy(sizePolicy)
|
|
206
259
|
self.enabled_checkbox.setMinimumSize(QSize(20, 0))
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
self.plugin_name =
|
|
210
|
-
|
|
260
|
+
|
|
261
|
+
# Plugin name
|
|
262
|
+
self.plugin_name = ClickableLabel(self) # To style content
|
|
263
|
+
font_plugin_name = QFont()
|
|
264
|
+
font_plugin_name.setPointSize(15)
|
|
265
|
+
font_plugin_name.setUnderline(True)
|
|
266
|
+
self.plugin_name.setFont(font_plugin_name)
|
|
267
|
+
|
|
268
|
+
# Status
|
|
269
|
+
self.status_icon = QLabel(self)
|
|
270
|
+
self.status_icon.setVisible(False)
|
|
271
|
+
self.status_label = QLabel(self)
|
|
272
|
+
self.status_label.setVisible(False)
|
|
273
|
+
|
|
211
274
|
if self.url and self.url != 'UNKNOWN':
|
|
275
|
+
# Do not want to highlight on hover unless there is a website.
|
|
212
276
|
self.plugin_name.setObjectName('plugin_name_web')
|
|
213
277
|
else:
|
|
214
278
|
self.plugin_name.setObjectName('plugin_name')
|
|
@@ -220,14 +284,11 @@ class PluginListItem(QFrame):
|
|
|
220
284
|
self.plugin_name.sizePolicy().hasHeightForWidth()
|
|
221
285
|
)
|
|
222
286
|
self.plugin_name.setSizePolicy(sizePolicy)
|
|
223
|
-
font15 = QFont()
|
|
224
|
-
font15.setPointSize(15)
|
|
225
|
-
font15.setUnderline(True)
|
|
226
|
-
self.plugin_name.setFont(font15)
|
|
227
|
-
self.row1.addWidget(self.plugin_name)
|
|
228
287
|
|
|
288
|
+
# Warning icon
|
|
229
289
|
icon = QColoredSVGIcon.from_resources("warning")
|
|
230
290
|
self.warning_tooltip = QtToolTipLabel(self)
|
|
291
|
+
|
|
231
292
|
# TODO: This color should come from the theme but the theme needs
|
|
232
293
|
# to provide the right color. Default warning should be orange, not
|
|
233
294
|
# red. Code example:
|
|
@@ -237,67 +298,58 @@ class PluginListItem(QFrame):
|
|
|
237
298
|
icon.colored(color="#E3B617").pixmap(15, 15)
|
|
238
299
|
)
|
|
239
300
|
self.warning_tooltip.setVisible(False)
|
|
240
|
-
self.row1.addWidget(self.warning_tooltip)
|
|
241
301
|
|
|
302
|
+
# Item status
|
|
242
303
|
self.item_status = QLabel(self)
|
|
243
304
|
self.item_status.setObjectName("small_italic_text")
|
|
244
305
|
self.item_status.setSizePolicy(sizePolicy)
|
|
245
|
-
self.row1.addWidget(self.item_status)
|
|
246
|
-
self.row1.addStretch()
|
|
247
|
-
self.v_lay.addLayout(self.row1)
|
|
248
306
|
|
|
249
|
-
|
|
250
|
-
self.error_indicator = QPushButton()
|
|
251
|
-
self.error_indicator.setObjectName("warning_icon")
|
|
252
|
-
self.error_indicator.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
253
|
-
self.error_indicator.hide()
|
|
254
|
-
self.row2.addWidget(
|
|
255
|
-
self.error_indicator,
|
|
256
|
-
0,
|
|
257
|
-
0,
|
|
258
|
-
1,
|
|
259
|
-
1,
|
|
260
|
-
alignment=Qt.AlignmentFlag.AlignTop,
|
|
261
|
-
)
|
|
262
|
-
self.row2.setSpacing(4)
|
|
307
|
+
# Summary
|
|
263
308
|
self.summary = QElidingLabel(parent=self)
|
|
264
309
|
self.summary.setObjectName('summary_text')
|
|
265
310
|
self.summary.setWordWrap(True)
|
|
266
311
|
|
|
267
|
-
|
|
312
|
+
font_summary = QFont()
|
|
313
|
+
font_summary.setPointSize(10)
|
|
314
|
+
self.summary.setFont(font_summary)
|
|
268
315
|
|
|
316
|
+
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
|
269
317
|
sizePolicy.setHorizontalStretch(1)
|
|
270
318
|
sizePolicy.setVerticalStretch(0)
|
|
271
319
|
self.summary.setSizePolicy(sizePolicy)
|
|
272
|
-
self.
|
|
273
|
-
self.summary, 0, 1, 1, 3, alignment=Qt.AlignmentFlag.AlignTop
|
|
274
|
-
)
|
|
320
|
+
self.summary.setContentsMargins(0, -2, 0, -2)
|
|
275
321
|
|
|
322
|
+
# Package author
|
|
276
323
|
self.package_author = QElidingLabel(self)
|
|
277
324
|
self.package_author.setObjectName('author_text')
|
|
278
325
|
self.package_author.setWordWrap(True)
|
|
279
326
|
self.package_author.setSizePolicy(sizePolicy)
|
|
280
|
-
self.row2.addWidget(
|
|
281
|
-
self.package_author,
|
|
282
|
-
0,
|
|
283
|
-
4,
|
|
284
|
-
1,
|
|
285
|
-
2,
|
|
286
|
-
alignment=Qt.AlignmentFlag.AlignTop,
|
|
287
|
-
)
|
|
288
327
|
|
|
328
|
+
# Update button
|
|
289
329
|
self.update_btn = QPushButton('Update', self)
|
|
290
|
-
sizePolicy.setRetainSizeWhenHidden(True)
|
|
291
|
-
self.update_btn.setSizePolicy(sizePolicy)
|
|
292
330
|
self.update_btn.setObjectName("install_button")
|
|
293
331
|
self.update_btn.setVisible(False)
|
|
332
|
+
self.update_btn.clicked.connect(self._update_requested)
|
|
333
|
+
sizePolicy.setRetainSizeWhenHidden(True)
|
|
334
|
+
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
|
335
|
+
self.update_btn.setSizePolicy(sizePolicy)
|
|
336
|
+
self.update_btn.clicked.connect(self._update_requested)
|
|
294
337
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
)
|
|
338
|
+
# Action Button
|
|
339
|
+
self.action_button = QPushButton(self)
|
|
340
|
+
self.action_button.setFixedWidth(70)
|
|
341
|
+
sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
|
342
|
+
self.action_button.setSizePolicy(sizePolicy1)
|
|
343
|
+
self.action_button.clicked.connect(self._action_requested)
|
|
298
344
|
|
|
299
|
-
|
|
300
|
-
self.
|
|
345
|
+
# Cancel
|
|
346
|
+
self.cancel_btn = QPushButton("Cancel", self)
|
|
347
|
+
self.cancel_btn.setObjectName("remove_button")
|
|
348
|
+
self.cancel_btn.setSizePolicy(sizePolicy)
|
|
349
|
+
self.cancel_btn.setFixedWidth(70)
|
|
350
|
+
self.cancel_btn.clicked.connect(self._cancel_requested)
|
|
351
|
+
|
|
352
|
+
# Collapsible button
|
|
301
353
|
coll_icon = QColoredSVGIcon.from_resources('right_arrow').colored(
|
|
302
354
|
color='white',
|
|
303
355
|
)
|
|
@@ -307,12 +359,11 @@ class PluginListItem(QFrame):
|
|
|
307
359
|
self.install_info_button = QCollapsible(
|
|
308
360
|
"Installation Info", collapsedIcon=coll_icon, expandedIcon=exp_icon
|
|
309
361
|
)
|
|
362
|
+
self.install_info_button.setLayoutDirection(
|
|
363
|
+
Qt.RightToLeft
|
|
364
|
+
) # Make icon appear on the right
|
|
310
365
|
self.install_info_button.setObjectName("install_info_button")
|
|
311
|
-
|
|
312
|
-
# To make the icon appear on the right
|
|
313
|
-
self.install_info_button.setLayoutDirection(Qt.RightToLeft)
|
|
314
|
-
|
|
315
|
-
# Remove any extra margins
|
|
366
|
+
self.install_info_button.setFixedWidth(180)
|
|
316
367
|
self.install_info_button.content().layout().setContentsMargins(
|
|
317
368
|
0, 0, 0, 0
|
|
318
369
|
)
|
|
@@ -322,77 +373,116 @@ class PluginListItem(QFrame):
|
|
|
322
373
|
self.install_info_button.layout().setSpacing(2)
|
|
323
374
|
self.install_info_button.setSizePolicy(sizePolicy)
|
|
324
375
|
|
|
376
|
+
# Information widget for available packages
|
|
377
|
+
self.info_choice_wdg = QWidget(self)
|
|
378
|
+
self.info_choice_wdg.setObjectName('install_choice')
|
|
379
|
+
|
|
325
380
|
self.source_choice_text = QLabel('Source:')
|
|
326
381
|
self.version_choice_text = QLabel('Version:')
|
|
327
382
|
self.source_choice_dropdown = QComboBox()
|
|
383
|
+
self.version_choice_dropdown = QComboBox()
|
|
328
384
|
|
|
329
|
-
if
|
|
330
|
-
self.source_choice_dropdown.addItem(PYPI)
|
|
331
|
-
|
|
332
|
-
if len(self._versions_conda) is not None:
|
|
385
|
+
if IS_NAPARI_CONDA_INSTALLED and self._versions_conda:
|
|
333
386
|
self.source_choice_dropdown.addItem(CONDA)
|
|
334
387
|
|
|
388
|
+
if self._versions_pypi:
|
|
389
|
+
self.source_choice_dropdown.addItem(PYPI)
|
|
390
|
+
|
|
391
|
+
source = self.get_installer_source()
|
|
392
|
+
self.source_choice_dropdown.setCurrentText(source)
|
|
393
|
+
self._populate_version_dropdown(source)
|
|
335
394
|
self.source_choice_dropdown.currentTextChanged.connect(
|
|
336
395
|
self._populate_version_dropdown
|
|
337
396
|
)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
)
|
|
397
|
+
|
|
398
|
+
# Information widget for installed packages
|
|
399
|
+
self.info_widget = QWidget(self)
|
|
400
|
+
self.info_widget.setLayoutDirection(Qt.LeftToRight)
|
|
401
|
+
self.info_widget.setObjectName("info_widget")
|
|
402
|
+
self.info_widget.setFixedWidth(180)
|
|
403
|
+
|
|
404
|
+
self.source_text = QLabel('Source:')
|
|
405
|
+
self.source = QLabel(PYPI)
|
|
406
|
+
self.version_text = QLabel('Version:')
|
|
407
|
+
self.version = QElidingLabel()
|
|
408
|
+
self.version.setWordWrap(True)
|
|
347
409
|
|
|
348
410
|
info_layout = QGridLayout()
|
|
349
411
|
info_layout.setContentsMargins(0, 0, 0, 0)
|
|
350
412
|
info_layout.setVerticalSpacing(0)
|
|
351
|
-
info_layout.addWidget(self.
|
|
352
|
-
info_layout.addWidget(self.
|
|
353
|
-
info_layout.addWidget(self.
|
|
354
|
-
info_layout.addWidget(self.
|
|
355
|
-
self.
|
|
356
|
-
self.info_choice_wdg.setLayoutDirection(Qt.LeftToRight)
|
|
357
|
-
self.info_choice_wdg.setObjectName("install_choice_widget")
|
|
358
|
-
self.info_choice_wdg.hide()
|
|
413
|
+
info_layout.addWidget(self.source_text, 0, 0)
|
|
414
|
+
info_layout.addWidget(self.source, 1, 0)
|
|
415
|
+
info_layout.addWidget(self.version_text, 0, 1)
|
|
416
|
+
info_layout.addWidget(self.version, 1, 1)
|
|
417
|
+
self.info_widget.setLayout(info_layout)
|
|
359
418
|
|
|
360
|
-
|
|
361
|
-
self.
|
|
362
|
-
self.
|
|
363
|
-
self.
|
|
364
|
-
|
|
365
|
-
)
|
|
419
|
+
# Error indicator
|
|
420
|
+
self.error_indicator = QPushButton()
|
|
421
|
+
self.error_indicator.setObjectName("warning_icon")
|
|
422
|
+
self.error_indicator.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
423
|
+
self.error_indicator.hide()
|
|
366
424
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
425
|
+
# region - Layout
|
|
426
|
+
# -----------------------------------------------------------------
|
|
427
|
+
layout = QHBoxLayout()
|
|
428
|
+
layout.setSpacing(2)
|
|
429
|
+
layout_left = QVBoxLayout()
|
|
430
|
+
layout_right = QVBoxLayout()
|
|
431
|
+
layout_top = QHBoxLayout()
|
|
432
|
+
layout_bottom = QHBoxLayout()
|
|
433
|
+
layout_bottom.setSpacing(4)
|
|
434
|
+
|
|
435
|
+
layout_left.addWidget(
|
|
436
|
+
self.enabled_checkbox, alignment=Qt.AlignmentFlag.AlignTop
|
|
373
437
|
)
|
|
374
438
|
|
|
375
|
-
|
|
439
|
+
layout_right.addLayout(layout_top, 1)
|
|
440
|
+
layout_right.addLayout(layout_bottom, 100)
|
|
441
|
+
|
|
442
|
+
layout.addLayout(layout_left)
|
|
443
|
+
layout.addLayout(layout_right)
|
|
444
|
+
|
|
445
|
+
self.setLayout(layout)
|
|
446
|
+
|
|
447
|
+
layout_top.addWidget(self.plugin_name)
|
|
448
|
+
layout_top.addWidget(self.status_icon)
|
|
449
|
+
layout_top.addWidget(self.status_label)
|
|
450
|
+
layout_top.addWidget(self.item_status)
|
|
451
|
+
layout_top.addStretch()
|
|
452
|
+
|
|
453
|
+
layout_bottom.addWidget(
|
|
454
|
+
self.summary, alignment=Qt.AlignmentFlag.AlignTop, stretch=3
|
|
455
|
+
)
|
|
456
|
+
layout_bottom.addWidget(
|
|
457
|
+
self.package_author, alignment=Qt.AlignmentFlag.AlignTop, stretch=1
|
|
458
|
+
)
|
|
459
|
+
layout_bottom.addWidget(
|
|
460
|
+
self.update_btn, alignment=Qt.AlignmentFlag.AlignTop
|
|
461
|
+
)
|
|
462
|
+
layout_bottom.addWidget(
|
|
463
|
+
self.install_info_button, alignment=Qt.AlignmentFlag.AlignTop
|
|
464
|
+
)
|
|
465
|
+
layout_bottom.addWidget(
|
|
466
|
+
self.action_button, alignment=Qt.AlignmentFlag.AlignTop
|
|
467
|
+
)
|
|
468
|
+
layout_bottom.addWidget(
|
|
469
|
+
self.cancel_btn, alignment=Qt.AlignmentFlag.AlignTop
|
|
470
|
+
)
|
|
376
471
|
|
|
377
|
-
self.info_widget = QWidget(self)
|
|
378
|
-
self.info_widget.setLayoutDirection(Qt.LeftToRight)
|
|
379
|
-
self.info_widget.setObjectName("info_widget")
|
|
380
472
|
info_layout = QGridLayout()
|
|
381
473
|
info_layout.setContentsMargins(0, 0, 0, 0)
|
|
382
474
|
info_layout.setVerticalSpacing(0)
|
|
383
|
-
self.
|
|
384
|
-
self.
|
|
385
|
-
self.
|
|
386
|
-
self.
|
|
475
|
+
info_layout.addWidget(self.source_choice_text, 0, 0, 1, 1)
|
|
476
|
+
info_layout.addWidget(self.source_choice_dropdown, 1, 0, 1, 1)
|
|
477
|
+
info_layout.addWidget(self.version_choice_text, 0, 1, 1, 1)
|
|
478
|
+
info_layout.addWidget(self.version_choice_dropdown, 1, 1, 1, 1)
|
|
387
479
|
|
|
388
|
-
|
|
389
|
-
info_layout.addWidget(self.source, 1, 0)
|
|
390
|
-
info_layout.addWidget(self.version_text, 0, 1)
|
|
391
|
-
info_layout.addWidget(self.package_name, 1, 1)
|
|
480
|
+
# endregion - Layout
|
|
392
481
|
|
|
393
|
-
self.
|
|
394
|
-
self.
|
|
395
|
-
self.
|
|
482
|
+
self.info_choice_wdg.setLayout(info_layout)
|
|
483
|
+
self.info_choice_wdg.setLayoutDirection(Qt.LeftToRight)
|
|
484
|
+
self.info_choice_wdg.setObjectName("install_choice_widget")
|
|
485
|
+
self.info_choice_wdg.hide()
|
|
396
486
|
|
|
397
487
|
def _populate_version_dropdown(self, source: Literal["PyPI", "Conda"]):
|
|
398
488
|
"""Display the versions available after selecting a source: pypi or conda."""
|
|
@@ -424,20 +514,66 @@ class PluginListItem(QFrame):
|
|
|
424
514
|
)
|
|
425
515
|
return
|
|
426
516
|
|
|
517
|
+
def _cancel_requested(self):
|
|
518
|
+
version = self.version_choice_dropdown.currentText()
|
|
519
|
+
tool = self.get_installer_tool()
|
|
520
|
+
self.actionRequested.emit(
|
|
521
|
+
self.item, self.name, InstallerActions.CANCEL, version, tool
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def _action_requested(self):
|
|
525
|
+
version = self.version_choice_dropdown.currentText()
|
|
526
|
+
tool = self.get_installer_tool()
|
|
527
|
+
action = (
|
|
528
|
+
InstallerActions.INSTALL
|
|
529
|
+
if self.action_button.objectName() == 'install_button'
|
|
530
|
+
else InstallerActions.UNINSTALL
|
|
531
|
+
)
|
|
532
|
+
self.actionRequested.emit(self.item, self.name, action, version, tool)
|
|
533
|
+
|
|
534
|
+
def _update_requested(self):
|
|
535
|
+
version = self.version_choice_dropdown.currentText()
|
|
536
|
+
tool = self.get_installer_tool()
|
|
537
|
+
self.actionRequested.emit(
|
|
538
|
+
self.item, self.name, InstallerActions.UPGRADE, version, tool
|
|
539
|
+
)
|
|
540
|
+
|
|
427
541
|
def show_warning(self, message: str = ""):
|
|
428
542
|
"""Show warning icon and tooltip."""
|
|
429
543
|
self.warning_tooltip.setVisible(bool(message))
|
|
430
544
|
self.warning_tooltip.setToolTip(message)
|
|
431
545
|
|
|
546
|
+
def get_installer_source(self):
|
|
547
|
+
return (
|
|
548
|
+
CONDA
|
|
549
|
+
if self.source_choice_dropdown.currentText() == CONDA
|
|
550
|
+
or is_conda_package(self.name)
|
|
551
|
+
else PYPI
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def get_installer_tool(self):
|
|
555
|
+
return (
|
|
556
|
+
InstallerTools.CONDA
|
|
557
|
+
if self.source_choice_dropdown.currentText() == CONDA
|
|
558
|
+
or is_conda_package(self.name, prefix=self.prefix)
|
|
559
|
+
else InstallerTools.PIP
|
|
560
|
+
)
|
|
561
|
+
|
|
432
562
|
|
|
433
563
|
class QPluginList(QListWidget):
|
|
564
|
+
|
|
565
|
+
_SORT_ORDER_PREFIX = '0-'
|
|
566
|
+
|
|
434
567
|
def __init__(self, parent: QWidget, installer: InstallerQueue) -> None:
|
|
435
568
|
super().__init__(parent)
|
|
436
569
|
self.installer = installer
|
|
437
|
-
self.setSortingEnabled(True)
|
|
438
570
|
self._remove_list = []
|
|
571
|
+
self._data = []
|
|
572
|
+
self._initial_height = None
|
|
573
|
+
|
|
574
|
+
self.setSortingEnabled(True)
|
|
439
575
|
|
|
440
|
-
def
|
|
576
|
+
def count_visible(self) -> int:
|
|
441
577
|
"""Return the number of visible items.
|
|
442
578
|
|
|
443
579
|
Visible items are the result of the normal `count` method minus
|
|
@@ -454,17 +590,13 @@ class QPluginList(QListWidget):
|
|
|
454
590
|
@Slot(tuple)
|
|
455
591
|
def addItem(
|
|
456
592
|
self,
|
|
457
|
-
|
|
458
|
-
npe2.PackageMetadata, List[str], List[str]
|
|
459
|
-
],
|
|
593
|
+
project_info: ProjectInfoVersions,
|
|
460
594
|
installed=False,
|
|
461
595
|
plugin_name=None,
|
|
462
596
|
enabled=True,
|
|
463
597
|
npe_version=None,
|
|
464
598
|
):
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
pkg_name = project_info.name
|
|
599
|
+
pkg_name = project_info.metadata.name
|
|
468
600
|
# don't add duplicates
|
|
469
601
|
if (
|
|
470
602
|
self.findItems(pkg_name, Qt.MatchFlag.MatchFixedString)
|
|
@@ -473,116 +605,102 @@ class QPluginList(QListWidget):
|
|
|
473
605
|
return
|
|
474
606
|
|
|
475
607
|
# including summary here for sake of filtering below.
|
|
476
|
-
searchable_text = f"{pkg_name} {project_info.summary}"
|
|
608
|
+
searchable_text = f"{pkg_name} {project_info.display_name} {project_info.metadata.summary}"
|
|
477
609
|
item = QListWidgetItem(searchable_text, self)
|
|
478
|
-
item.version = project_info.version
|
|
610
|
+
item.version = project_info.metadata.version
|
|
479
611
|
super().addItem(item)
|
|
480
612
|
widg = PluginListItem(
|
|
613
|
+
item=item,
|
|
481
614
|
package_name=pkg_name,
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
615
|
+
display_name=project_info.display_name,
|
|
616
|
+
version=project_info.metadata.version,
|
|
617
|
+
url=project_info.metadata.home_page,
|
|
618
|
+
summary=project_info.metadata.summary,
|
|
619
|
+
author=project_info.metadata.author,
|
|
620
|
+
license=project_info.metadata.license,
|
|
487
621
|
parent=self,
|
|
488
622
|
plugin_name=plugin_name,
|
|
489
623
|
enabled=enabled,
|
|
490
624
|
installed=installed,
|
|
491
625
|
npe_version=npe_version,
|
|
492
|
-
versions_conda=
|
|
493
|
-
versions_pypi=
|
|
626
|
+
versions_conda=project_info.conda_versions,
|
|
627
|
+
versions_pypi=project_info.pypi_versions,
|
|
494
628
|
)
|
|
495
629
|
item.widget = widg
|
|
496
630
|
item.npe_version = npe_version
|
|
497
|
-
action_name = 'uninstall' if installed else 'install'
|
|
498
631
|
item.setSizeHint(widg.sizeHint())
|
|
499
632
|
self.setItemWidget(item, widg)
|
|
500
633
|
|
|
501
|
-
if project_info.home_page:
|
|
502
|
-
import webbrowser
|
|
503
|
-
|
|
504
|
-
# FIXME: Partial may lead to leak memory when connecting to Qt signals.
|
|
634
|
+
if project_info.metadata.home_page:
|
|
505
635
|
widg.plugin_name.clicked.connect(
|
|
506
|
-
partial(webbrowser.open, project_info.home_page)
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
# FIXME: Partial may lead to leak memory when connecting to Qt signals.
|
|
510
|
-
widg.action_button.clicked.connect(
|
|
511
|
-
partial(
|
|
512
|
-
self.handle_action,
|
|
513
|
-
item,
|
|
514
|
-
pkg_name,
|
|
515
|
-
action_name,
|
|
516
|
-
version=widg.version_choice_dropdown.currentText(),
|
|
517
|
-
installer_choice=widg.source_choice_dropdown.currentText(),
|
|
518
|
-
)
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
widg.update_btn.clicked.connect(
|
|
522
|
-
partial(
|
|
523
|
-
self.handle_action,
|
|
524
|
-
item,
|
|
525
|
-
pkg_name,
|
|
526
|
-
InstallerActions.UPGRADE,
|
|
636
|
+
partial(webbrowser.open, project_info.metadata.home_page)
|
|
527
637
|
)
|
|
528
|
-
)
|
|
529
|
-
widg.cancel_btn.clicked.connect(
|
|
530
|
-
partial(
|
|
531
|
-
self.handle_action, item, pkg_name, InstallerActions.CANCEL
|
|
532
|
-
)
|
|
533
|
-
)
|
|
534
638
|
|
|
639
|
+
widg.actionRequested.connect(self.handle_action)
|
|
535
640
|
item.setSizeHint(item.widget.size())
|
|
641
|
+
if self._initial_height is None:
|
|
642
|
+
self._initial_height = item.widget.size().height()
|
|
643
|
+
|
|
536
644
|
widg.install_info_button.setDuration(0)
|
|
537
645
|
widg.install_info_button.toggled.connect(
|
|
538
646
|
lambda: self._resize_pluginlistitem(item)
|
|
539
647
|
)
|
|
540
648
|
|
|
649
|
+
def removeItem(self, name):
|
|
650
|
+
count = self.count()
|
|
651
|
+
for i in range(count):
|
|
652
|
+
item = self.item(i)
|
|
653
|
+
if item.widget.name == name:
|
|
654
|
+
self.takeItem(i)
|
|
655
|
+
break
|
|
656
|
+
|
|
657
|
+
def refreshItem(self, name, version=None):
|
|
658
|
+
count = self.count()
|
|
659
|
+
for i in range(count):
|
|
660
|
+
item = self.item(i)
|
|
661
|
+
if item.widget.name == name:
|
|
662
|
+
if version is not None:
|
|
663
|
+
item.version = version
|
|
664
|
+
mod_version = version.replace('.', '․') # noqa: RUF001
|
|
665
|
+
item.widget.version.setText(mod_version)
|
|
666
|
+
item.widget.version.setToolTip(version)
|
|
667
|
+
item.widget.set_busy('', InstallerActions.CANCEL)
|
|
668
|
+
if item.text().startswith(self._SORT_ORDER_PREFIX):
|
|
669
|
+
item.setText(item.text()[len(self._SORT_ORDER_PREFIX) :])
|
|
670
|
+
break
|
|
671
|
+
|
|
541
672
|
def _resize_pluginlistitem(self, item):
|
|
542
673
|
"""Resize the plugin list item, especially after toggling QCollapsible."""
|
|
543
|
-
height = item.widget.height()
|
|
544
674
|
if item.widget.install_info_button.isExpanded():
|
|
545
|
-
item.widget.setFixedHeight(
|
|
675
|
+
item.widget.setFixedHeight(self._initial_height + 35)
|
|
546
676
|
else:
|
|
547
|
-
item.widget.setFixedHeight(
|
|
548
|
-
|
|
677
|
+
item.widget.setFixedHeight(self._initial_height)
|
|
678
|
+
|
|
679
|
+
item.setSizeHint(QSize(0, item.widget.height()))
|
|
549
680
|
|
|
550
681
|
def handle_action(
|
|
551
682
|
self,
|
|
552
683
|
item: QListWidgetItem,
|
|
553
684
|
pkg_name: str,
|
|
554
685
|
action_name: InstallerActions,
|
|
555
|
-
version: str = None,
|
|
686
|
+
version: Optional[str] = None,
|
|
556
687
|
installer_choice: Optional[str] = None,
|
|
557
688
|
):
|
|
558
689
|
"""Determine which action is called (install, uninstall, update, cancel).
|
|
559
690
|
Update buttons appropriately and run the action."""
|
|
560
|
-
tool = (
|
|
561
|
-
InstallerTools.CONDA
|
|
562
|
-
if item.widget.source_choice_dropdown.currentText() == CONDA
|
|
563
|
-
or is_conda_package(pkg_name)
|
|
564
|
-
else InstallerTools.PIP
|
|
565
|
-
)
|
|
566
|
-
|
|
567
691
|
widget = item.widget
|
|
568
|
-
|
|
692
|
+
tool = installer_choice or widget.get_installer_tool()
|
|
569
693
|
self._remove_list.append((pkg_name, item))
|
|
570
694
|
self._warn_dialog = None
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
# show warning pop up dialog
|
|
574
|
-
message = trans._(
|
|
575
|
-
'When installing/uninstalling npe2 plugins, you must '
|
|
576
|
-
'restart napari for UI changes to take effect.'
|
|
577
|
-
)
|
|
578
|
-
self._warn_dialog = WarnPopup(text=message)
|
|
695
|
+
if not item.text().startswith(self._SORT_ORDER_PREFIX):
|
|
696
|
+
item.setText(f"{self._SORT_ORDER_PREFIX}{item.text()}")
|
|
579
697
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
698
|
+
# TODO: NPE version unknown before installing
|
|
699
|
+
if (
|
|
700
|
+
widget.npe_version != 1
|
|
701
|
+
and action_name == InstallerActions.UNINSTALL
|
|
702
|
+
):
|
|
703
|
+
_show_message(widget)
|
|
586
704
|
|
|
587
705
|
if action_name == InstallerActions.INSTALL:
|
|
588
706
|
if version:
|
|
@@ -596,6 +714,7 @@ class QPluginList(QListWidget):
|
|
|
596
714
|
pkgs=[pkg_name],
|
|
597
715
|
# origins="TODO",
|
|
598
716
|
)
|
|
717
|
+
widget.setProperty("current_job_id", job_id)
|
|
599
718
|
if self._warn_dialog:
|
|
600
719
|
self._warn_dialog.exec_()
|
|
601
720
|
self.scrollToTop()
|
|
@@ -605,6 +724,7 @@ class QPluginList(QListWidget):
|
|
|
605
724
|
pkg_name += f"=={item.latest_version}"
|
|
606
725
|
|
|
607
726
|
widget.set_busy(trans._("updating..."), action_name)
|
|
727
|
+
widget.update_btn.setDisabled(True)
|
|
608
728
|
widget.action_button.setDisabled(True)
|
|
609
729
|
|
|
610
730
|
job_id = self.installer.upgrade(
|
|
@@ -612,6 +732,7 @@ class QPluginList(QListWidget):
|
|
|
612
732
|
pkgs=[pkg_name],
|
|
613
733
|
# origins="TODO",
|
|
614
734
|
)
|
|
735
|
+
widget.setProperty("current_job_id", job_id)
|
|
615
736
|
if self._warn_dialog:
|
|
616
737
|
self._warn_dialog.exec_()
|
|
617
738
|
self.scrollToTop()
|
|
@@ -637,10 +758,17 @@ class QPluginList(QListWidget):
|
|
|
637
758
|
finally:
|
|
638
759
|
widget.setProperty("current_job_id", None)
|
|
639
760
|
|
|
761
|
+
def set_data(self, data):
|
|
762
|
+
self._data = data
|
|
763
|
+
|
|
764
|
+
def is_running(self):
|
|
765
|
+
return self.count() != len(self._data)
|
|
766
|
+
|
|
767
|
+
def packages(self):
|
|
768
|
+
return [self.item(idx).widget.name for idx in range(self.count())]
|
|
769
|
+
|
|
640
770
|
@Slot(npe2.PackageMetadata, bool)
|
|
641
|
-
def tag_outdated(
|
|
642
|
-
self, project_info: npe2.PackageMetadata, is_available: bool
|
|
643
|
-
):
|
|
771
|
+
def tag_outdated(self, metadata: npe2.PackageMetadata, is_available: bool):
|
|
644
772
|
"""Determines if an installed plugin is up to date with the latest version.
|
|
645
773
|
If it is not, the latest version will be displayed on the update button.
|
|
646
774
|
"""
|
|
@@ -648,13 +776,21 @@ class QPluginList(QListWidget):
|
|
|
648
776
|
return
|
|
649
777
|
|
|
650
778
|
for item in self.findItems(
|
|
651
|
-
|
|
779
|
+
metadata.name, Qt.MatchFlag.MatchStartsWith
|
|
652
780
|
):
|
|
653
781
|
current = item.version
|
|
654
|
-
latest =
|
|
782
|
+
latest = metadata.version
|
|
783
|
+
is_marked_outdated = getattr(item, 'outdated', False)
|
|
655
784
|
if parse_version(current) >= parse_version(latest):
|
|
785
|
+
# currently is up to date
|
|
786
|
+
if is_marked_outdated:
|
|
787
|
+
# previously marked as outdated, need to update item
|
|
788
|
+
# `outdated` state and hide item widget `update_btn`
|
|
789
|
+
item.outdated = False
|
|
790
|
+
widg = self.itemWidget(item)
|
|
791
|
+
widg.update_btn.setVisible(False)
|
|
656
792
|
continue
|
|
657
|
-
if
|
|
793
|
+
if is_marked_outdated:
|
|
658
794
|
# already tagged it
|
|
659
795
|
continue
|
|
660
796
|
|
|
@@ -666,7 +802,7 @@ class QPluginList(QListWidget):
|
|
|
666
802
|
trans._("update (v{latest})", latest=latest)
|
|
667
803
|
)
|
|
668
804
|
|
|
669
|
-
def tag_unavailable(self,
|
|
805
|
+
def tag_unavailable(self, metadata: npe2.PackageMetadata):
|
|
670
806
|
"""
|
|
671
807
|
Tag list items as unavailable for install with conda-forge.
|
|
672
808
|
|
|
@@ -674,7 +810,7 @@ class QPluginList(QListWidget):
|
|
|
674
810
|
icon with a hover tooltip.
|
|
675
811
|
"""
|
|
676
812
|
for item in self.findItems(
|
|
677
|
-
|
|
813
|
+
metadata.name, Qt.MatchFlag.MatchStartsWith
|
|
678
814
|
):
|
|
679
815
|
widget = self.itemWidget(item)
|
|
680
816
|
widget.show_warning(
|
|
@@ -688,15 +824,28 @@ class QPluginList(QListWidget):
|
|
|
688
824
|
widget.action_button.setEnabled(False)
|
|
689
825
|
widget.warning_tooltip.setVisible(True)
|
|
690
826
|
|
|
691
|
-
def filter(self, text: str):
|
|
827
|
+
def filter(self, text: str, starts_with_chars: int = 1):
|
|
692
828
|
"""Filter items to those containing `text`."""
|
|
693
829
|
if text:
|
|
694
830
|
# PySide has some issues, so we compare using id
|
|
695
831
|
# See: https://bugreports.qt.io/browse/PYSIDE-74
|
|
696
|
-
|
|
832
|
+
flag = (
|
|
833
|
+
Qt.MatchFlag.MatchStartsWith
|
|
834
|
+
if len(text) <= starts_with_chars
|
|
835
|
+
else Qt.MatchFlag.MatchContains
|
|
836
|
+
)
|
|
837
|
+
if len(text) <= starts_with_chars:
|
|
838
|
+
flag = Qt.MatchFlag.MatchStartsWith
|
|
839
|
+
queries = (text, f'napari-{text}')
|
|
840
|
+
else:
|
|
841
|
+
flag = Qt.MatchFlag.MatchContains
|
|
842
|
+
queries = (text,)
|
|
843
|
+
|
|
844
|
+
shown = {
|
|
697
845
|
id(it)
|
|
698
|
-
for
|
|
699
|
-
|
|
846
|
+
for query in queries
|
|
847
|
+
for it in self.findItems(query, flag)
|
|
848
|
+
}
|
|
700
849
|
for i in range(self.count()):
|
|
701
850
|
item = self.item(i)
|
|
702
851
|
item.setHidden(id(item) not in shown)
|
|
@@ -706,35 +855,93 @@ class QPluginList(QListWidget):
|
|
|
706
855
|
item.setHidden(False)
|
|
707
856
|
|
|
708
857
|
|
|
709
|
-
class RefreshState(Enum):
|
|
710
|
-
REFRESHING = auto()
|
|
711
|
-
OUTDATED = auto()
|
|
712
|
-
DONE = auto()
|
|
713
|
-
|
|
714
|
-
|
|
715
858
|
class QtPluginDialog(QDialog):
|
|
716
|
-
def __init__(self, parent=None) -> None:
|
|
859
|
+
def __init__(self, parent=None, prefix=None) -> None:
|
|
717
860
|
super().__init__(parent)
|
|
718
|
-
|
|
861
|
+
|
|
862
|
+
self._parent = parent
|
|
863
|
+
if (
|
|
864
|
+
parent is not None
|
|
865
|
+
and getattr(parent, '_plugin_dialog', None) is None
|
|
866
|
+
):
|
|
867
|
+
self._parent._plugin_dialog = self
|
|
868
|
+
|
|
719
869
|
self.already_installed = set()
|
|
720
870
|
self.available_set = set()
|
|
721
|
-
|
|
722
|
-
self.
|
|
723
|
-
self.
|
|
871
|
+
self._prefix = prefix
|
|
872
|
+
self._first_open = True
|
|
873
|
+
self._plugin_queue = [] # Store plugin data to be added
|
|
874
|
+
self._plugin_data = [] # Store all plugin data
|
|
875
|
+
self._filter_texts = []
|
|
876
|
+
self._filter_idxs_cache = set()
|
|
877
|
+
self._filter_timer = QTimer(self)
|
|
878
|
+
self.worker = None
|
|
879
|
+
|
|
880
|
+
# timer to avoid triggering a filter for every keystroke
|
|
881
|
+
self._filter_timer.setInterval(140) # ms
|
|
882
|
+
self._filter_timer.timeout.connect(self.filter)
|
|
883
|
+
self._filter_timer.setSingleShot(True)
|
|
884
|
+
self._plugin_data_map = {}
|
|
724
885
|
self._add_items_timer = QTimer(self)
|
|
725
|
-
|
|
726
|
-
|
|
886
|
+
|
|
887
|
+
# Timer to avoid race conditions and incorrect count of plugins when
|
|
888
|
+
# refreshing multiple times in a row. After click we disable the
|
|
889
|
+
# `Refresh` button and re-enable it after 3 seconds.
|
|
890
|
+
self._refresh_timer = QTimer(self)
|
|
891
|
+
self._refresh_timer.setInterval(3000) # ms
|
|
892
|
+
self._refresh_timer.setSingleShot(True)
|
|
893
|
+
self._refresh_timer.timeout.connect(self._enable_refresh_button)
|
|
894
|
+
|
|
895
|
+
# Add items in batches with a pause to avoid blocking the UI
|
|
896
|
+
self._add_items_timer.setInterval(61) # ms
|
|
727
897
|
self._add_items_timer.timeout.connect(self._add_items)
|
|
728
|
-
self._add_items_timer.timeout.connect(self._update_count_in_label)
|
|
729
898
|
|
|
730
|
-
self.installer = InstallerQueue()
|
|
899
|
+
self.installer = InstallerQueue(parent=self, prefix=prefix)
|
|
731
900
|
self.setWindowTitle(trans._('Plugin Manager'))
|
|
732
|
-
self.
|
|
733
|
-
self.setWindowTitle('Plugin Manager')
|
|
901
|
+
self._setup_ui()
|
|
734
902
|
self.installer.set_output_widget(self.stdout_text)
|
|
735
903
|
self.installer.started.connect(self._on_installer_start)
|
|
736
|
-
self.installer.
|
|
737
|
-
self.
|
|
904
|
+
self.installer.processFinished.connect(self._on_process_finished)
|
|
905
|
+
self.installer.allFinished.connect(self._on_installer_all_finished)
|
|
906
|
+
self.setAcceptDrops(True)
|
|
907
|
+
|
|
908
|
+
if (
|
|
909
|
+
parent is not None and parent._plugin_dialog is self
|
|
910
|
+
) or parent is None:
|
|
911
|
+
self.refresh()
|
|
912
|
+
self._setup_shortcuts()
|
|
913
|
+
|
|
914
|
+
# region - Private methods
|
|
915
|
+
# ------------------------------------------------------------------------
|
|
916
|
+
def _enable_refresh_button(self):
|
|
917
|
+
self.refresh_button.setEnabled(True)
|
|
918
|
+
|
|
919
|
+
def _quit(self):
|
|
920
|
+
self.close()
|
|
921
|
+
with contextlib.suppress(AttributeError):
|
|
922
|
+
self._parent.close(quit_app=True, confirm_need=True)
|
|
923
|
+
|
|
924
|
+
def _setup_shortcuts(self):
|
|
925
|
+
self._refresh_styles_action = QAction(trans._('Refresh Styles'), self)
|
|
926
|
+
self._refresh_styles_action.setShortcut('Ctrl+R')
|
|
927
|
+
self._refresh_styles_action.triggered.connect(self._update_theme)
|
|
928
|
+
self.addAction(self._refresh_styles_action)
|
|
929
|
+
|
|
930
|
+
self._quit_action = QAction(trans._('Exit'), self)
|
|
931
|
+
self._quit_action.setShortcut('Ctrl+Q')
|
|
932
|
+
self._quit_action.setMenuRole(QAction.QuitRole)
|
|
933
|
+
self._quit_action.triggered.connect(self._quit)
|
|
934
|
+
self.addAction(self._quit_action)
|
|
935
|
+
|
|
936
|
+
self._close_shortcut = QShortcut(
|
|
937
|
+
QKeySequence(Qt.CTRL | Qt.Key_W), self
|
|
938
|
+
)
|
|
939
|
+
self._close_shortcut.activated.connect(self.close)
|
|
940
|
+
get_settings().appearance.events.theme.connect(self._update_theme)
|
|
941
|
+
|
|
942
|
+
def _update_theme(self, event):
|
|
943
|
+
stylesheet = get_current_stylesheet([STYLES_PATH])
|
|
944
|
+
self.setStyleSheet(stylesheet)
|
|
738
945
|
|
|
739
946
|
def _on_installer_start(self):
|
|
740
947
|
"""Updates dialog buttons and status when installing a plugin."""
|
|
@@ -742,73 +949,115 @@ class QtPluginDialog(QDialog):
|
|
|
742
949
|
self.working_indicator.show()
|
|
743
950
|
self.process_success_indicator.hide()
|
|
744
951
|
self.process_error_indicator.hide()
|
|
745
|
-
self.
|
|
952
|
+
self.refresh_button.setDisabled(True)
|
|
953
|
+
|
|
954
|
+
def _on_process_finished(self, process_finished_data: ProcessFinishedData):
|
|
955
|
+
action = process_finished_data['action']
|
|
956
|
+
exit_code = process_finished_data['exit_code']
|
|
957
|
+
pkg_names = [
|
|
958
|
+
pkg.split('==')[0] for pkg in process_finished_data['pkgs']
|
|
959
|
+
]
|
|
960
|
+
if action == InstallerActions.INSTALL:
|
|
961
|
+
if exit_code == 0:
|
|
962
|
+
for pkg_name in pkg_names:
|
|
963
|
+
if pkg_name in self.available_set:
|
|
964
|
+
self.available_set.remove(pkg_name)
|
|
965
|
+
|
|
966
|
+
self.available_list.removeItem(pkg_name)
|
|
967
|
+
self._add_installed(pkg_name)
|
|
968
|
+
self._tag_outdated_plugins()
|
|
969
|
+
else:
|
|
970
|
+
for pkg_name in pkg_names:
|
|
971
|
+
self.available_list.refreshItem(pkg_name)
|
|
972
|
+
elif action == InstallerActions.UNINSTALL:
|
|
973
|
+
if exit_code == 0:
|
|
974
|
+
for pkg_name in pkg_names:
|
|
975
|
+
if pkg_name in self.already_installed:
|
|
976
|
+
self.already_installed.remove(pkg_name)
|
|
977
|
+
|
|
978
|
+
self.installed_list.removeItem(pkg_name)
|
|
979
|
+
self._add_to_available(pkg_name)
|
|
980
|
+
else:
|
|
981
|
+
for pkg_name in pkg_names:
|
|
982
|
+
self.installed_list.refreshItem(pkg_name)
|
|
983
|
+
elif action == InstallerActions.UPGRADE:
|
|
984
|
+
pkg_info = [
|
|
985
|
+
(pkg.split('==')[0], pkg.split('==')[1])
|
|
986
|
+
for pkg in process_finished_data['pkgs']
|
|
987
|
+
]
|
|
988
|
+
for pkg_name, pkg_version in pkg_info:
|
|
989
|
+
self.installed_list.refreshItem(pkg_name, version=pkg_version)
|
|
990
|
+
self._tag_outdated_plugins()
|
|
991
|
+
elif action in [InstallerActions.CANCEL, InstallerActions.CANCEL_ALL]:
|
|
992
|
+
for pkg_name in pkg_names:
|
|
993
|
+
self.installed_list.refreshItem(pkg_name)
|
|
994
|
+
self.available_list.refreshItem(pkg_name)
|
|
995
|
+
self._tag_outdated_plugins()
|
|
746
996
|
|
|
747
|
-
def _on_installer_done(self, exit_code):
|
|
748
|
-
"""Updates buttons and status when plugin is done installing."""
|
|
749
997
|
self.working_indicator.hide()
|
|
750
998
|
if exit_code:
|
|
751
999
|
self.process_error_indicator.show()
|
|
752
1000
|
else:
|
|
753
1001
|
self.process_success_indicator.show()
|
|
1002
|
+
|
|
1003
|
+
def _on_installer_all_finished(self, exit_codes):
|
|
1004
|
+
self.working_indicator.hide()
|
|
754
1005
|
self.cancel_all_btn.setVisible(False)
|
|
755
1006
|
self.close_btn.setDisabled(False)
|
|
756
|
-
self.
|
|
1007
|
+
self.refresh_button.setDisabled(False)
|
|
757
1008
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1009
|
+
if not self.isVisible():
|
|
1010
|
+
if sum(exit_codes) > 0:
|
|
1011
|
+
show_warning(
|
|
1012
|
+
trans._('Plugin Manager: process completed with errors\n')
|
|
1013
|
+
)
|
|
1014
|
+
else:
|
|
1015
|
+
show_info(trans._('Plugin Manager: process completed\n'))
|
|
763
1016
|
|
|
764
|
-
def
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
self.
|
|
769
|
-
self.installed_list.clear()
|
|
770
|
-
self.available_list.clear()
|
|
1017
|
+
def _add_to_available(self, pkg_name):
|
|
1018
|
+
self._add_items_timer.stop()
|
|
1019
|
+
self._plugin_queue.insert(0, self._plugin_data_map[pkg_name])
|
|
1020
|
+
self._add_items_timer.start()
|
|
1021
|
+
self._update_plugin_count()
|
|
771
1022
|
|
|
772
|
-
|
|
773
|
-
|
|
1023
|
+
def _add_to_installed(self, distname, enabled, npe_version=1):
|
|
1024
|
+
norm_name = normalized_name(distname or '')
|
|
1025
|
+
if distname:
|
|
1026
|
+
try:
|
|
1027
|
+
meta = importlib.metadata.metadata(distname)
|
|
774
1028
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
metadata_version="1.0",
|
|
795
|
-
name=norm_name,
|
|
796
|
-
version=meta.get('version', ''),
|
|
797
|
-
summary=meta.get('summary', ''),
|
|
798
|
-
home_page=meta.get('Home-page', ''),
|
|
799
|
-
author=meta.get('author', ''),
|
|
800
|
-
license=meta.get('license', ''),
|
|
801
|
-
),
|
|
802
|
-
[],
|
|
803
|
-
[],
|
|
1029
|
+
except importlib.metadata.PackageNotFoundError:
|
|
1030
|
+
return # a race condition has occurred and the package is uninstalled by another thread
|
|
1031
|
+
if len(meta) == 0:
|
|
1032
|
+
# will not add builtins.
|
|
1033
|
+
return
|
|
1034
|
+
self.already_installed.add(norm_name)
|
|
1035
|
+
else:
|
|
1036
|
+
meta = {}
|
|
1037
|
+
|
|
1038
|
+
self.installed_list.addItem(
|
|
1039
|
+
ProjectInfoVersions(
|
|
1040
|
+
npe2.PackageMetadata(
|
|
1041
|
+
metadata_version="1.0",
|
|
1042
|
+
name=norm_name,
|
|
1043
|
+
version=meta.get('version', ''),
|
|
1044
|
+
summary=meta.get('summary', ''),
|
|
1045
|
+
home_page=meta.get('Home-page', ''),
|
|
1046
|
+
author=meta.get('author', ''),
|
|
1047
|
+
license=meta.get('license', ''),
|
|
804
1048
|
),
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
)
|
|
1049
|
+
norm_name,
|
|
1050
|
+
[],
|
|
1051
|
+
[],
|
|
1052
|
+
),
|
|
1053
|
+
installed=True,
|
|
1054
|
+
enabled=enabled,
|
|
1055
|
+
npe_version=npe_version,
|
|
1056
|
+
)
|
|
809
1057
|
|
|
1058
|
+
def _add_installed(self, pkg_name=None):
|
|
810
1059
|
pm2 = npe2.PluginManager.instance()
|
|
811
|
-
|
|
1060
|
+
pm2.discover()
|
|
812
1061
|
for manifest in pm2.iter_manifests():
|
|
813
1062
|
distname = normalized_name(manifest.name or '')
|
|
814
1063
|
if distname in self.already_installed or distname == 'napari':
|
|
@@ -816,7 +1065,8 @@ class QtPluginDialog(QDialog):
|
|
|
816
1065
|
enabled = not pm2.is_disabled(manifest.name)
|
|
817
1066
|
# if it's an Npe1 adaptor, call it v1
|
|
818
1067
|
npev = 'shim' if manifest.npe1_shim else 2
|
|
819
|
-
|
|
1068
|
+
if distname == pkg_name or pkg_name is None:
|
|
1069
|
+
self._add_to_installed(distname, enabled, npe_version=npev)
|
|
820
1070
|
|
|
821
1071
|
napari.plugins.plugin_manager.discover() # since they might not be loaded yet
|
|
822
1072
|
for (
|
|
@@ -825,50 +1075,49 @@ class QtPluginDialog(QDialog):
|
|
|
825
1075
|
distname,
|
|
826
1076
|
) in napari.plugins.plugin_manager.iter_available():
|
|
827
1077
|
# not showing these in the plugin dialog
|
|
828
|
-
if plugin_name in (
|
|
1078
|
+
if plugin_name in (
|
|
1079
|
+
'napari_plugin_engine',
|
|
1080
|
+
'napari_plugin_manager',
|
|
1081
|
+
):
|
|
829
1082
|
continue
|
|
830
1083
|
if normalized_name(distname or '') in self.already_installed:
|
|
831
1084
|
continue
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
self.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1085
|
+
if normalized_name(distname or '') == pkg_name or pkg_name is None:
|
|
1086
|
+
self._add_to_installed(
|
|
1087
|
+
distname,
|
|
1088
|
+
not napari.plugins.plugin_manager.is_blocked(plugin_name),
|
|
1089
|
+
)
|
|
1090
|
+
self._update_plugin_count()
|
|
1091
|
+
|
|
1092
|
+
for i in range(self.installed_list.count()):
|
|
1093
|
+
item = self.installed_list.item(i)
|
|
1094
|
+
widget = item.widget
|
|
1095
|
+
if widget.name == pkg_name:
|
|
1096
|
+
self.installed_list.scrollToItem(item)
|
|
1097
|
+
self.installed_list.setCurrentItem(item)
|
|
1098
|
+
if widget.npe_version != 1:
|
|
1099
|
+
_show_message(widget)
|
|
1100
|
+
break
|
|
1101
|
+
|
|
1102
|
+
def _fetch_available_plugins(self, clear_cache: bool = False):
|
|
845
1103
|
get_settings()
|
|
846
1104
|
|
|
847
|
-
|
|
1105
|
+
if clear_cache:
|
|
1106
|
+
cache_clear()
|
|
848
1107
|
|
|
1108
|
+
self.worker = create_worker(iter_napari_plugin_info)
|
|
849
1109
|
self.worker.yielded.connect(self._handle_yield)
|
|
1110
|
+
self.worker.started.connect(self.working_indicator.show)
|
|
850
1111
|
self.worker.finished.connect(self.working_indicator.hide)
|
|
851
|
-
self.worker.finished.connect(self.
|
|
1112
|
+
self.worker.finished.connect(self._add_items_timer.start)
|
|
852
1113
|
self.worker.start()
|
|
853
|
-
self._add_items_timer.start()
|
|
854
1114
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
'When installing/uninstalling npe2 plugins, '
|
|
858
|
-
'you must restart napari for UI changes to take effect.'
|
|
859
|
-
)
|
|
860
|
-
self._warn_dialog = WarnPopup(text=message)
|
|
861
|
-
global_point = self.process_error_indicator.mapToGlobal(
|
|
862
|
-
self.process_error_indicator.rect().topLeft()
|
|
863
|
-
)
|
|
864
|
-
global_point = QPoint(global_point.x(), global_point.y() - 75)
|
|
865
|
-
self._warn_dialog.move(global_point)
|
|
866
|
-
self._warn_dialog.exec_()
|
|
1115
|
+
pm2 = npe2.PluginManager.instance()
|
|
1116
|
+
pm2.discover()
|
|
867
1117
|
|
|
868
|
-
def
|
|
1118
|
+
def _setup_ui(self):
|
|
869
1119
|
"""Defines the layout for the PluginDialog."""
|
|
870
|
-
|
|
871
|
-
self.resize(950, 640)
|
|
1120
|
+
self.resize(900, 600)
|
|
872
1121
|
vlay_1 = QVBoxLayout(self)
|
|
873
1122
|
self.h_splitter = QSplitter(self)
|
|
874
1123
|
vlay_1.addWidget(self.h_splitter)
|
|
@@ -885,13 +1134,28 @@ class QtPluginDialog(QDialog):
|
|
|
885
1134
|
self.packages_filter.setPlaceholderText(trans._("filter..."))
|
|
886
1135
|
self.packages_filter.setMaximumWidth(350)
|
|
887
1136
|
self.packages_filter.setClearButtonEnabled(True)
|
|
1137
|
+
self.packages_filter.textChanged.connect(self._filter_timer.start)
|
|
1138
|
+
|
|
1139
|
+
self.refresh_button = QPushButton(trans._('Refresh'), self)
|
|
1140
|
+
self.refresh_button.setObjectName("refresh_button")
|
|
1141
|
+
self.refresh_button.setToolTip(
|
|
1142
|
+
trans._(
|
|
1143
|
+
'This will clear and refresh the available and installed plugins lists.'
|
|
1144
|
+
)
|
|
1145
|
+
)
|
|
1146
|
+
self.refresh_button.clicked.connect(self._refresh_and_clear_cache)
|
|
1147
|
+
|
|
888
1148
|
mid_layout = QVBoxLayout()
|
|
889
|
-
|
|
1149
|
+
horizontal_mid_layout = QHBoxLayout()
|
|
1150
|
+
horizontal_mid_layout.addWidget(self.packages_filter)
|
|
1151
|
+
horizontal_mid_layout.addStretch()
|
|
1152
|
+
horizontal_mid_layout.addWidget(self.refresh_button)
|
|
1153
|
+
mid_layout.addLayout(horizontal_mid_layout)
|
|
1154
|
+
# mid_layout.addWidget(self.packages_filter)
|
|
890
1155
|
mid_layout.addWidget(self.installed_label)
|
|
891
1156
|
lay.addLayout(mid_layout)
|
|
892
1157
|
|
|
893
1158
|
self.installed_list = QPluginList(installed, self.installer)
|
|
894
|
-
self.packages_filter.textChanged.connect(self.installed_list.filter)
|
|
895
1159
|
lay.addWidget(self.installed_list)
|
|
896
1160
|
|
|
897
1161
|
uninstalled = QWidget(self.v_splitter)
|
|
@@ -903,7 +1167,6 @@ class QtPluginDialog(QDialog):
|
|
|
903
1167
|
mid_layout.addStretch()
|
|
904
1168
|
lay.addLayout(mid_layout)
|
|
905
1169
|
self.available_list = QPluginList(uninstalled, self.installer)
|
|
906
|
-
self.packages_filter.textChanged.connect(self.available_list.filter)
|
|
907
1170
|
lay.addWidget(self.available_list)
|
|
908
1171
|
|
|
909
1172
|
self.stdout_text = QTextEdit(self.v_splitter)
|
|
@@ -931,13 +1194,34 @@ class QtPluginDialog(QDialog):
|
|
|
931
1194
|
visibility_direct_entry = not running_as_constructor_app()
|
|
932
1195
|
self.direct_entry_edit = QLineEdit(self)
|
|
933
1196
|
self.direct_entry_edit.installEventFilter(self)
|
|
934
|
-
self.direct_entry_edit.
|
|
935
|
-
trans._('install by name/url, or drop file...')
|
|
936
|
-
)
|
|
1197
|
+
self.direct_entry_edit.returnPressed.connect(self._install_packages)
|
|
937
1198
|
self.direct_entry_edit.setVisible(visibility_direct_entry)
|
|
938
|
-
self.direct_entry_btn =
|
|
1199
|
+
self.direct_entry_btn = QToolButton(self)
|
|
939
1200
|
self.direct_entry_btn.setVisible(visibility_direct_entry)
|
|
940
1201
|
self.direct_entry_btn.clicked.connect(self._install_packages)
|
|
1202
|
+
self.direct_entry_btn.setText(trans._("Install"))
|
|
1203
|
+
|
|
1204
|
+
self._action_conda = QAction(trans._('Conda'), self)
|
|
1205
|
+
self._action_conda.setCheckable(True)
|
|
1206
|
+
self._action_conda.triggered.connect(self._update_direct_entry_text)
|
|
1207
|
+
|
|
1208
|
+
self._action_pypi = QAction(trans._('pip'), self)
|
|
1209
|
+
self._action_pypi.setCheckable(True)
|
|
1210
|
+
self._action_pypi.triggered.connect(self._update_direct_entry_text)
|
|
1211
|
+
|
|
1212
|
+
self._action_group = QActionGroup(self)
|
|
1213
|
+
self._action_group.addAction(self._action_pypi)
|
|
1214
|
+
self._action_group.addAction(self._action_conda)
|
|
1215
|
+
self._action_group.setExclusive(True)
|
|
1216
|
+
|
|
1217
|
+
self._menu = QMenu(self)
|
|
1218
|
+
self._menu.addAction(self._action_conda)
|
|
1219
|
+
self._menu.addAction(self._action_pypi)
|
|
1220
|
+
|
|
1221
|
+
if IS_NAPARI_CONDA_INSTALLED:
|
|
1222
|
+
self.direct_entry_btn.setPopupMode(QToolButton.MenuButtonPopup)
|
|
1223
|
+
self._action_conda.setChecked(True)
|
|
1224
|
+
self.direct_entry_btn.setMenu(self._menu)
|
|
941
1225
|
|
|
942
1226
|
self.show_status_btn = QPushButton(trans._("Show Status"), self)
|
|
943
1227
|
self.show_status_btn.setFixedWidth(100)
|
|
@@ -945,11 +1229,12 @@ class QtPluginDialog(QDialog):
|
|
|
945
1229
|
self.cancel_all_btn = QPushButton(trans._("cancel all actions"), self)
|
|
946
1230
|
self.cancel_all_btn.setObjectName("remove_button")
|
|
947
1231
|
self.cancel_all_btn.setVisible(False)
|
|
948
|
-
self.cancel_all_btn.clicked.connect(self.installer.
|
|
1232
|
+
self.cancel_all_btn.clicked.connect(self.installer.cancel_all)
|
|
949
1233
|
|
|
950
1234
|
self.close_btn = QPushButton(trans._("Close"), self)
|
|
951
1235
|
self.close_btn.clicked.connect(self.accept)
|
|
952
1236
|
self.close_btn.setObjectName("close_button")
|
|
1237
|
+
|
|
953
1238
|
buttonBox.addWidget(self.show_status_btn)
|
|
954
1239
|
buttonBox.addWidget(self.working_indicator)
|
|
955
1240
|
buttonBox.addWidget(self.direct_entry_edit)
|
|
@@ -967,52 +1252,70 @@ class QtPluginDialog(QDialog):
|
|
|
967
1252
|
|
|
968
1253
|
self.show_status_btn.setCheckable(True)
|
|
969
1254
|
self.show_status_btn.setChecked(False)
|
|
970
|
-
self.show_status_btn.toggled.connect(self.
|
|
1255
|
+
self.show_status_btn.toggled.connect(self.toggle_status)
|
|
971
1256
|
|
|
972
1257
|
self.v_splitter.setStretchFactor(1, 2)
|
|
973
1258
|
self.h_splitter.setStretchFactor(0, 2)
|
|
974
1259
|
|
|
975
1260
|
self.packages_filter.setFocus()
|
|
1261
|
+
self._update_direct_entry_text()
|
|
976
1262
|
|
|
977
|
-
def
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1263
|
+
def _update_direct_entry_text(self):
|
|
1264
|
+
tool = (
|
|
1265
|
+
str(InstallerTools.CONDA)
|
|
1266
|
+
if self._action_conda.isChecked()
|
|
1267
|
+
else str(InstallerTools.PIP)
|
|
1268
|
+
)
|
|
1269
|
+
self.direct_entry_edit.setPlaceholderText(
|
|
1270
|
+
trans._(
|
|
1271
|
+
"install with '{tool}' by name/url, or drop file...", tool=tool
|
|
1272
|
+
)
|
|
983
1273
|
)
|
|
984
1274
|
|
|
985
|
-
def
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1275
|
+
def _update_plugin_count(self):
|
|
1276
|
+
"""Update count labels for both installed and available plugin lists.
|
|
1277
|
+
Displays also amount of visible plugins out of total when filtering.
|
|
1278
|
+
"""
|
|
1279
|
+
installed_count = self.installed_list.count()
|
|
1280
|
+
installed_count_visible = self.installed_list.count_visible()
|
|
1281
|
+
if installed_count == installed_count_visible:
|
|
1282
|
+
self.installed_label.setText(
|
|
1283
|
+
trans._(
|
|
1284
|
+
"Installed Plugins ({amount})",
|
|
1285
|
+
amount=installed_count,
|
|
1286
|
+
)
|
|
1287
|
+
)
|
|
1288
|
+
else:
|
|
1289
|
+
self.installed_label.setText(
|
|
1290
|
+
trans._(
|
|
1291
|
+
"Installed Plugins ({count}/{amount})",
|
|
1292
|
+
count=installed_count_visible,
|
|
1293
|
+
amount=installed_count,
|
|
1294
|
+
)
|
|
1295
|
+
)
|
|
990
1296
|
|
|
991
|
-
|
|
992
|
-
if
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
return True
|
|
1002
|
-
return super().eventFilter(watched, event)
|
|
1003
|
-
|
|
1004
|
-
def _toggle_status(self, show):
|
|
1005
|
-
if show:
|
|
1006
|
-
self.show_status_btn.setText(trans._("Hide Status"))
|
|
1007
|
-
self.stdout_text.show()
|
|
1297
|
+
available_count = len(self._plugin_data) - self.installed_list.count()
|
|
1298
|
+
available_count = available_count if available_count >= 0 else 0
|
|
1299
|
+
available_count_visible = self.available_list.count_visible()
|
|
1300
|
+
if available_count == available_count_visible:
|
|
1301
|
+
self.avail_label.setText(
|
|
1302
|
+
trans._(
|
|
1303
|
+
"Available Plugins ({amount})",
|
|
1304
|
+
amount=available_count,
|
|
1305
|
+
)
|
|
1306
|
+
)
|
|
1008
1307
|
else:
|
|
1009
|
-
self.
|
|
1010
|
-
|
|
1308
|
+
self.avail_label.setText(
|
|
1309
|
+
trans._(
|
|
1310
|
+
"Available Plugins ({count}/{amount})",
|
|
1311
|
+
count=available_count_visible,
|
|
1312
|
+
amount=available_count,
|
|
1313
|
+
)
|
|
1314
|
+
)
|
|
1011
1315
|
|
|
1012
1316
|
def _install_packages(
|
|
1013
1317
|
self,
|
|
1014
1318
|
packages: Sequence[str] = (),
|
|
1015
|
-
versions: Optional[Sequence[str]] = None,
|
|
1016
1319
|
):
|
|
1017
1320
|
if not packages:
|
|
1018
1321
|
_packages = self.direct_entry_edit.text()
|
|
@@ -1020,40 +1323,75 @@ class QtPluginDialog(QDialog):
|
|
|
1020
1323
|
[_packages] if os.path.exists(_packages) else _packages.split()
|
|
1021
1324
|
)
|
|
1022
1325
|
self.direct_entry_edit.clear()
|
|
1326
|
+
|
|
1023
1327
|
if packages:
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1328
|
+
tool = (
|
|
1329
|
+
InstallerTools.CONDA
|
|
1330
|
+
if self._action_conda.isChecked()
|
|
1331
|
+
else InstallerTools.PIP
|
|
1027
1332
|
)
|
|
1333
|
+
self.installer.install(tool, packages)
|
|
1334
|
+
|
|
1335
|
+
def _tag_outdated_plugins(self):
|
|
1336
|
+
"""Tag installed plugins that might be outdated."""
|
|
1337
|
+
for pkg_name in self.installed_list.packages():
|
|
1338
|
+
_data = self._plugin_data_map.get(pkg_name)
|
|
1339
|
+
if _data is not None:
|
|
1340
|
+
metadata, is_available_in_conda, _ = _data
|
|
1341
|
+
self.installed_list.tag_outdated(
|
|
1342
|
+
metadata, is_available_in_conda
|
|
1343
|
+
)
|
|
1028
1344
|
|
|
1029
1345
|
def _add_items(self):
|
|
1030
|
-
"""
|
|
1031
|
-
|
|
1346
|
+
"""
|
|
1347
|
+
Add items to the lists by `batch_size` using a timer to add a pause
|
|
1348
|
+
and prevent freezing the UI.
|
|
1349
|
+
"""
|
|
1350
|
+
if len(self._plugin_queue) == 0:
|
|
1032
1351
|
if (
|
|
1033
1352
|
self.installed_list.count() + self.available_list.count()
|
|
1034
|
-
== len(self.
|
|
1353
|
+
== len(self._plugin_data)
|
|
1354
|
+
and self.available_list.count() != 0
|
|
1035
1355
|
):
|
|
1036
1356
|
self._add_items_timer.stop()
|
|
1357
|
+
if not self.isVisible():
|
|
1358
|
+
show_info(
|
|
1359
|
+
trans._(
|
|
1360
|
+
'Plugin Manager: All available plugins loaded\n'
|
|
1361
|
+
)
|
|
1362
|
+
)
|
|
1363
|
+
|
|
1037
1364
|
return
|
|
1038
1365
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if
|
|
1045
|
-
self.
|
|
1046
|
-
|
|
1047
|
-
(
|
|
1048
|
-
project_info,
|
|
1049
|
-
extra_info['pypi_versions'],
|
|
1050
|
-
extra_info['conda_versions'],
|
|
1051
|
-
)
|
|
1366
|
+
batch_size = 2
|
|
1367
|
+
for _ in range(batch_size):
|
|
1368
|
+
data = self._plugin_queue.pop(0)
|
|
1369
|
+
metadata, is_available_in_conda, extra_info = data
|
|
1370
|
+
display_name = extra_info.get('display_name', metadata.name)
|
|
1371
|
+
if metadata.name in self.already_installed:
|
|
1372
|
+
self.installed_list.tag_outdated(
|
|
1373
|
+
metadata, is_available_in_conda
|
|
1052
1374
|
)
|
|
1053
|
-
|
|
1054
|
-
self.
|
|
1375
|
+
else:
|
|
1376
|
+
if metadata.name not in self.available_set:
|
|
1377
|
+
self.available_set.add(metadata.name)
|
|
1378
|
+
self.available_list.addItem(
|
|
1379
|
+
ProjectInfoVersions(
|
|
1380
|
+
metadata,
|
|
1381
|
+
display_name,
|
|
1382
|
+
extra_info['pypi_versions'],
|
|
1383
|
+
extra_info['conda_versions'],
|
|
1384
|
+
)
|
|
1385
|
+
)
|
|
1386
|
+
if ON_BUNDLE and not is_available_in_conda:
|
|
1387
|
+
self.available_list.tag_unavailable(metadata)
|
|
1388
|
+
|
|
1389
|
+
if len(self._plugin_queue) == 0:
|
|
1390
|
+
self._tag_outdated_plugins()
|
|
1391
|
+
break
|
|
1055
1392
|
|
|
1056
|
-
self.
|
|
1393
|
+
if not self._filter_timer.isActive():
|
|
1394
|
+
self.filter(None, skip=True)
|
|
1057
1395
|
|
|
1058
1396
|
def _handle_yield(self, data: Tuple[npe2.PackageMetadata, bool, Dict]):
|
|
1059
1397
|
"""Output from a worker process.
|
|
@@ -1064,23 +1402,149 @@ class QtPluginDialog(QDialog):
|
|
|
1064
1402
|
method to prevent the UI from freezing by adding all items at once.
|
|
1065
1403
|
"""
|
|
1066
1404
|
self._plugin_data.append(data)
|
|
1067
|
-
self.
|
|
1405
|
+
self._plugin_queue.append(data)
|
|
1406
|
+
self._filter_texts = [
|
|
1407
|
+
f"{i[0].name} {i[-1].get('display_name', '')} {i[0].summary}".lower()
|
|
1408
|
+
for i in self._plugin_data
|
|
1409
|
+
]
|
|
1410
|
+
metadata, _, _ = data
|
|
1411
|
+
self._plugin_data_map[metadata.name] = data
|
|
1412
|
+
self.available_list.set_data(self._plugin_data)
|
|
1413
|
+
|
|
1414
|
+
def _search_in_available(self, text):
|
|
1415
|
+
idxs = []
|
|
1416
|
+
for idx, item in enumerate(self._filter_texts):
|
|
1417
|
+
if text.lower() in item and idx not in self._filter_idxs_cache:
|
|
1418
|
+
idxs.append(idx)
|
|
1419
|
+
self._filter_idxs_cache.add(idx)
|
|
1420
|
+
|
|
1421
|
+
return idxs
|
|
1422
|
+
|
|
1423
|
+
def _refresh_and_clear_cache(self):
|
|
1424
|
+
self.refresh(clear_cache=True)
|
|
1425
|
+
|
|
1426
|
+
# endregion - Private methods
|
|
1427
|
+
|
|
1428
|
+
# region - Qt overrides
|
|
1429
|
+
# ------------------------------------------------------------------------
|
|
1430
|
+
def closeEvent(self, event):
|
|
1431
|
+
if self._parent is not None:
|
|
1432
|
+
plugin_dialog = getattr(self._parent, '_plugin_dialog', self)
|
|
1433
|
+
if self != plugin_dialog:
|
|
1434
|
+
self.destroy(True, True)
|
|
1435
|
+
super().closeEvent(event)
|
|
1436
|
+
else:
|
|
1437
|
+
plugin_dialog.hide()
|
|
1438
|
+
else:
|
|
1439
|
+
super().closeEvent(event)
|
|
1440
|
+
|
|
1441
|
+
def dragEnterEvent(self, event):
|
|
1442
|
+
event.accept()
|
|
1443
|
+
|
|
1444
|
+
def dropEvent(self, event):
|
|
1445
|
+
md = event.mimeData()
|
|
1446
|
+
if md.hasUrls():
|
|
1447
|
+
files = [url.toLocalFile() for url in md.urls()]
|
|
1448
|
+
self.direct_entry_edit.setText(files[0])
|
|
1449
|
+
return True
|
|
1068
1450
|
|
|
1069
|
-
|
|
1451
|
+
return super().dropEvent(event)
|
|
1452
|
+
|
|
1453
|
+
def exec_(self):
|
|
1454
|
+
plugin_dialog = getattr(self._parent, '_plugin_dialog', self)
|
|
1455
|
+
if plugin_dialog != self:
|
|
1456
|
+
self.close()
|
|
1457
|
+
|
|
1458
|
+
plugin_dialog.setModal(True)
|
|
1459
|
+
plugin_dialog.show()
|
|
1460
|
+
|
|
1461
|
+
if self._first_open:
|
|
1462
|
+
self._update_theme(None)
|
|
1463
|
+
self._first_open = False
|
|
1464
|
+
|
|
1465
|
+
def hideEvent(self, event):
|
|
1466
|
+
self.packages_filter.clear()
|
|
1467
|
+
self.toggle_status(False)
|
|
1468
|
+
super().hideEvent(event)
|
|
1469
|
+
|
|
1470
|
+
# endregion - Qt overrides
|
|
1471
|
+
|
|
1472
|
+
# region - Public methods
|
|
1473
|
+
# ------------------------------------------------------------------------
|
|
1474
|
+
def filter(self, text: Optional[str] = None, skip=False) -> None:
|
|
1070
1475
|
"""Filter by text or set current text as filter."""
|
|
1071
1476
|
if text is None:
|
|
1072
1477
|
text = self.packages_filter.text()
|
|
1073
1478
|
else:
|
|
1074
1479
|
self.packages_filter.setText(text)
|
|
1075
1480
|
|
|
1481
|
+
if not skip and self.available_list.is_running() and len(text) >= 1:
|
|
1482
|
+
items = [
|
|
1483
|
+
self._plugin_data[idx]
|
|
1484
|
+
for idx in self._search_in_available(text)
|
|
1485
|
+
]
|
|
1486
|
+
if items:
|
|
1487
|
+
for item in items:
|
|
1488
|
+
if item in self._plugin_queue:
|
|
1489
|
+
self._plugin_queue.remove(item)
|
|
1490
|
+
|
|
1491
|
+
self._plugin_queue = items + self._plugin_queue
|
|
1492
|
+
|
|
1076
1493
|
self.installed_list.filter(text)
|
|
1077
1494
|
self.available_list.filter(text)
|
|
1495
|
+
self._update_plugin_count()
|
|
1496
|
+
|
|
1497
|
+
def refresh(self, clear_cache: bool = False):
|
|
1498
|
+
self.refresh_button.setDisabled(True)
|
|
1499
|
+
|
|
1500
|
+
if self.worker is not None:
|
|
1501
|
+
self.worker.quit()
|
|
1502
|
+
|
|
1503
|
+
if self._add_items_timer.isActive():
|
|
1504
|
+
self._add_items_timer.stop()
|
|
1505
|
+
|
|
1506
|
+
self._filter_texts = []
|
|
1507
|
+
self._plugin_queue = []
|
|
1508
|
+
self._plugin_data = []
|
|
1509
|
+
self._plugin_data_map = {}
|
|
1510
|
+
|
|
1511
|
+
self.installed_list.clear()
|
|
1512
|
+
self.available_list.clear()
|
|
1513
|
+
self.already_installed = set()
|
|
1514
|
+
self.available_set = set()
|
|
1515
|
+
|
|
1516
|
+
self._add_installed()
|
|
1517
|
+
self._fetch_available_plugins(clear_cache=clear_cache)
|
|
1518
|
+
|
|
1519
|
+
self._refresh_timer.start()
|
|
1520
|
+
|
|
1521
|
+
def toggle_status(self, show=None):
|
|
1522
|
+
show = not self.stdout_text.isVisible() if show is None else show
|
|
1523
|
+
if show:
|
|
1524
|
+
self.show_status_btn.setText(trans._("Hide Status"))
|
|
1525
|
+
self.stdout_text.show()
|
|
1526
|
+
else:
|
|
1527
|
+
self.show_status_btn.setText(trans._("Show Status"))
|
|
1528
|
+
self.stdout_text.hide()
|
|
1529
|
+
|
|
1530
|
+
def set_prefix(self, prefix):
|
|
1531
|
+
self._prefix = prefix
|
|
1532
|
+
self.installer._prefix = prefix
|
|
1533
|
+
for idx in range(self.available_list.count()):
|
|
1534
|
+
item = self.available_list.item(idx)
|
|
1535
|
+
item.widget.prefix = prefix
|
|
1536
|
+
|
|
1537
|
+
for idx in range(self.installed_list.count()):
|
|
1538
|
+
item = self.installed_list.item(idx)
|
|
1539
|
+
item.widget.prefix = prefix
|
|
1540
|
+
|
|
1541
|
+
# endregion - Public methods
|
|
1078
1542
|
|
|
1079
1543
|
|
|
1080
1544
|
if __name__ == "__main__":
|
|
1081
1545
|
from qtpy.QtWidgets import QApplication
|
|
1082
1546
|
|
|
1083
1547
|
app = QApplication([])
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
app.exec_()
|
|
1548
|
+
widget = QtPluginDialog()
|
|
1549
|
+
widget.exec_()
|
|
1550
|
+
sys.exit(app.exec_())
|