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