napari-plugin-manager 0.1.2__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 +108 -84
- 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 -1359
- {napari_plugin_manager-0.1.2.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.2.dist-info → napari_plugin_manager-0.1.4.dist-info}/WHEEL +1 -1
- napari_plugin_manager-0.1.2.dist-info/RECORD +0 -19
- {napari_plugin_manager-0.1.2.dist-info → napari_plugin_manager-0.1.4.dist-info}/LICENSE +0 -0
- {napari_plugin_manager-0.1.2.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,62 +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
|
-
StringEnum,
|
|
21
|
-
parse_version,
|
|
22
14
|
running_as_constructor_app,
|
|
23
15
|
)
|
|
24
16
|
from napari.utils.notifications import show_info, show_warning
|
|
25
17
|
from napari.utils.translations import trans
|
|
26
|
-
from qtpy.QtCore import QPoint, QSize
|
|
18
|
+
from qtpy.QtCore import QPoint, QSize
|
|
27
19
|
from qtpy.QtGui import (
|
|
28
|
-
QAction,
|
|
29
|
-
QActionGroup,
|
|
30
|
-
QFont,
|
|
31
|
-
QKeySequence,
|
|
32
20
|
QMovie,
|
|
33
|
-
QShortcut,
|
|
34
21
|
)
|
|
35
|
-
from qtpy.QtWidgets import
|
|
36
|
-
QCheckBox,
|
|
37
|
-
QComboBox,
|
|
38
|
-
QDialog,
|
|
39
|
-
QFrame,
|
|
40
|
-
QGridLayout,
|
|
41
|
-
QHBoxLayout,
|
|
42
|
-
QLabel,
|
|
43
|
-
QLineEdit,
|
|
44
|
-
QListWidget,
|
|
45
|
-
QListWidgetItem,
|
|
46
|
-
QMenu,
|
|
47
|
-
QPushButton,
|
|
48
|
-
QSizePolicy,
|
|
49
|
-
QSplitter,
|
|
50
|
-
QTextEdit,
|
|
51
|
-
QToolButton,
|
|
52
|
-
QVBoxLayout,
|
|
53
|
-
QWidget,
|
|
54
|
-
)
|
|
55
|
-
from superqt import QCollapsible, QElidingLabel
|
|
22
|
+
from qtpy.QtWidgets import QCheckBox, QMessageBox
|
|
56
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
|
+
)
|
|
57
34
|
from napari_plugin_manager.npe2api import (
|
|
58
35
|
cache_clear,
|
|
59
36
|
iter_napari_plugin_info,
|
|
60
37
|
)
|
|
61
|
-
from napari_plugin_manager.qt_package_installer import
|
|
62
|
-
InstallerActions,
|
|
63
|
-
InstallerQueue,
|
|
64
|
-
InstallerTools,
|
|
65
|
-
ProcessFinishedData,
|
|
66
|
-
)
|
|
67
|
-
from napari_plugin_manager.qt_widgets import ClickableLabel
|
|
38
|
+
from napari_plugin_manager.qt_package_installer import NapariInstallerQueue
|
|
68
39
|
from napari_plugin_manager.utils import is_conda_package
|
|
69
40
|
|
|
70
41
|
# Scaling factor for each list widget item when expanding.
|
|
71
|
-
CONDA = 'Conda'
|
|
72
|
-
PYPI = 'PyPI'
|
|
73
|
-
ON_BUNDLE = running_as_constructor_app()
|
|
74
|
-
IS_NAPARI_CONDA_INSTALLED = is_conda_package('napari')
|
|
75
42
|
STYLES_PATH = Path(__file__).parent / 'styles.qss'
|
|
43
|
+
DISMISS_WARN_PYPI_INSTALL_DLG = False
|
|
76
44
|
|
|
77
45
|
|
|
78
46
|
def _show_message(widget):
|
|
@@ -93,406 +61,56 @@ def _show_message(widget):
|
|
|
93
61
|
warn_dialog.exec_()
|
|
94
62
|
|
|
95
63
|
|
|
96
|
-
class ProjectInfoVersions(
|
|
64
|
+
class ProjectInfoVersions(BaseProjectInfoVersions):
|
|
97
65
|
metadata: npe2.PackageMetadata
|
|
98
|
-
display_name: str
|
|
99
|
-
pypi_versions: List[str]
|
|
100
|
-
conda_versions: List[str]
|
|
101
66
|
|
|
102
67
|
|
|
103
|
-
class PluginListItem(
|
|
68
|
+
class PluginListItem(BasePluginListItem):
|
|
104
69
|
"""An entry in the plugin dialog. This will include the package name, summary,
|
|
105
70
|
author, source, version, and buttons to update, install/uninstall, etc."""
|
|
106
71
|
|
|
107
|
-
|
|
108
|
-
actionRequested = Signal(QListWidgetItem, str, StringEnum, str, StringEnum)
|
|
109
|
-
|
|
110
|
-
def __init__(
|
|
111
|
-
self,
|
|
112
|
-
item: QListWidgetItem,
|
|
113
|
-
package_name: str,
|
|
114
|
-
display_name: str,
|
|
115
|
-
version: str = '',
|
|
116
|
-
url: str = '',
|
|
117
|
-
summary: str = '',
|
|
118
|
-
author: str = '',
|
|
119
|
-
license: str = "UNKNOWN", # noqa: A002
|
|
120
|
-
*,
|
|
121
|
-
plugin_name: Optional[str] = None,
|
|
122
|
-
parent: QWidget = None,
|
|
123
|
-
enabled: bool = True,
|
|
124
|
-
installed: bool = False,
|
|
125
|
-
npe_version=1,
|
|
126
|
-
versions_conda: Optional[List[str]] = None,
|
|
127
|
-
versions_pypi: Optional[List[str]] = None,
|
|
128
|
-
prefix=None,
|
|
129
|
-
) -> None:
|
|
130
|
-
super().__init__(parent)
|
|
131
|
-
self.prefix = prefix
|
|
132
|
-
self.item = item
|
|
133
|
-
self.url = url
|
|
134
|
-
self.name = package_name
|
|
135
|
-
self.npe_version = npe_version
|
|
136
|
-
self._version = version
|
|
137
|
-
self._versions_conda = versions_conda
|
|
138
|
-
self._versions_pypi = versions_pypi
|
|
139
|
-
self.setup_ui(enabled)
|
|
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)
|
|
147
|
-
|
|
148
|
-
if len(versions_pypi) > 0:
|
|
149
|
-
self._populate_version_dropdown(PYPI)
|
|
150
|
-
else:
|
|
151
|
-
self._populate_version_dropdown(CONDA)
|
|
152
|
-
|
|
153
|
-
mod_version = version.replace('.', '․') # noqa: RUF001
|
|
154
|
-
self.version.setWordWrap(True)
|
|
155
|
-
self.version.setText(mod_version)
|
|
156
|
-
self.version.setToolTip(version)
|
|
157
|
-
|
|
158
|
-
if summary:
|
|
159
|
-
self.summary.setText(summary)
|
|
160
|
-
|
|
161
|
-
if author:
|
|
162
|
-
self.package_author.setText(author)
|
|
163
|
-
|
|
164
|
-
self.package_author.setWordWrap(True)
|
|
165
|
-
self.cancel_btn.setVisible(False)
|
|
166
|
-
|
|
167
|
-
self._handle_npe2_plugin(npe_version)
|
|
168
|
-
self._set_installed(installed, package_name)
|
|
169
|
-
self._populate_version_dropdown(self.get_installer_source())
|
|
170
|
-
|
|
171
|
-
def _set_installed(self, installed: bool, package_name):
|
|
172
|
-
if installed:
|
|
173
|
-
if is_conda_package(package_name):
|
|
174
|
-
self.source.setText(CONDA)
|
|
175
|
-
|
|
176
|
-
self.enabled_checkbox.show()
|
|
177
|
-
self.action_button.setText(trans._("Uninstall"))
|
|
178
|
-
self.action_button.setObjectName("remove_button")
|
|
179
|
-
self.info_choice_wdg.hide()
|
|
180
|
-
self.install_info_button.addWidget(self.info_widget)
|
|
181
|
-
self.info_widget.show()
|
|
182
|
-
else:
|
|
183
|
-
self.enabled_checkbox.hide()
|
|
184
|
-
self.action_button.setText(trans._("Install"))
|
|
185
|
-
self.action_button.setObjectName("install_button")
|
|
186
|
-
self.info_widget.hide()
|
|
187
|
-
self.install_info_button.addWidget(self.info_choice_wdg)
|
|
188
|
-
self.info_choice_wdg.show()
|
|
189
|
-
|
|
190
|
-
def _handle_npe2_plugin(self, npe_version):
|
|
191
|
-
if npe_version in (None, 1):
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
opacity = 0.4 if npe_version == 'shim' else 1
|
|
195
|
-
text = trans._('npe1 (adapted)') if npe_version == 'shim' else 'npe2'
|
|
196
|
-
icon = QColoredSVGIcon.from_resources('logo_silhouette')
|
|
197
|
-
self.set_status(
|
|
198
|
-
icon.colored(color='#33F0FF', opacity=opacity).pixmap(20, 20), text
|
|
199
|
-
)
|
|
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))
|
|
211
|
-
|
|
212
|
-
def set_busy(
|
|
213
|
-
self,
|
|
214
|
-
text: str,
|
|
215
|
-
action_name: Optional[
|
|
216
|
-
Literal['install', 'uninstall', 'cancel', 'upgrade']
|
|
217
|
-
] = None,
|
|
218
|
-
):
|
|
219
|
-
"""Updates status text and what buttons are visible when any button is pushed.
|
|
220
|
-
|
|
221
|
-
Parameters
|
|
222
|
-
----------
|
|
223
|
-
text: str
|
|
224
|
-
The new string to be displayed as the status.
|
|
225
|
-
action_name: str
|
|
226
|
-
The action of the button pressed.
|
|
227
|
-
|
|
228
|
-
"""
|
|
229
|
-
self.item_status.setText(text)
|
|
230
|
-
if action_name == 'upgrade':
|
|
231
|
-
self.cancel_btn.setVisible(True)
|
|
232
|
-
self.action_button.setVisible(False)
|
|
233
|
-
elif action_name in {'uninstall', 'install'}:
|
|
234
|
-
self.action_button.setVisible(False)
|
|
235
|
-
self.cancel_btn.setVisible(True)
|
|
236
|
-
elif action_name == 'cancel':
|
|
237
|
-
self.action_button.setVisible(True)
|
|
238
|
-
self.action_button.setDisabled(False)
|
|
239
|
-
self.cancel_btn.setVisible(False)
|
|
240
|
-
else: # pragma: no cover
|
|
241
|
-
raise ValueError(f"Not supported {action_name}")
|
|
242
|
-
|
|
243
|
-
def setup_ui(self, enabled=True):
|
|
244
|
-
"""Define the layout of the PluginListItem"""
|
|
245
|
-
# Enabled checkbox
|
|
246
|
-
self.enabled_checkbox = QCheckBox(self)
|
|
247
|
-
self.enabled_checkbox.setChecked(enabled)
|
|
248
|
-
self.enabled_checkbox.setToolTip(trans._("enable/disable"))
|
|
249
|
-
self.enabled_checkbox.setText("")
|
|
250
|
-
self.enabled_checkbox.stateChanged.connect(self._on_enabled_checkbox)
|
|
251
|
-
|
|
252
|
-
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
253
|
-
sizePolicy.setHorizontalStretch(0)
|
|
254
|
-
sizePolicy.setVerticalStretch(0)
|
|
255
|
-
sizePolicy.setHeightForWidth(
|
|
256
|
-
self.enabled_checkbox.sizePolicy().hasHeightForWidth()
|
|
257
|
-
)
|
|
258
|
-
self.enabled_checkbox.setSizePolicy(sizePolicy)
|
|
259
|
-
self.enabled_checkbox.setMinimumSize(QSize(20, 0))
|
|
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
|
-
|
|
274
|
-
if self.url and self.url != 'UNKNOWN':
|
|
275
|
-
# Do not want to highlight on hover unless there is a website.
|
|
276
|
-
self.plugin_name.setObjectName('plugin_name_web')
|
|
277
|
-
else:
|
|
278
|
-
self.plugin_name.setObjectName('plugin_name')
|
|
279
|
-
|
|
280
|
-
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
|
|
281
|
-
sizePolicy.setHorizontalStretch(0)
|
|
282
|
-
sizePolicy.setVerticalStretch(0)
|
|
283
|
-
sizePolicy.setHeightForWidth(
|
|
284
|
-
self.plugin_name.sizePolicy().hasHeightForWidth()
|
|
285
|
-
)
|
|
286
|
-
self.plugin_name.setSizePolicy(sizePolicy)
|
|
287
|
-
|
|
288
|
-
# Warning icon
|
|
289
|
-
icon = QColoredSVGIcon.from_resources("warning")
|
|
290
|
-
self.warning_tooltip = QtToolTipLabel(self)
|
|
72
|
+
BASE_PACKAGE_NAME = 'napari'
|
|
291
73
|
|
|
74
|
+
def _warning_icon(self):
|
|
292
75
|
# TODO: This color should come from the theme but the theme needs
|
|
293
76
|
# to provide the right color. Default warning should be orange, not
|
|
294
77
|
# red. Code example:
|
|
295
78
|
# theme_name = get_settings().appearance.theme
|
|
296
79
|
# napari.utils.theme.get_theme(theme_name, as_dict=False).warning.as_hex()
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
300
|
-
self.warning_tooltip.setVisible(False)
|
|
301
|
-
|
|
302
|
-
# Item status
|
|
303
|
-
self.item_status = QLabel(self)
|
|
304
|
-
self.item_status.setObjectName("small_italic_text")
|
|
305
|
-
self.item_status.setSizePolicy(sizePolicy)
|
|
306
|
-
|
|
307
|
-
# Summary
|
|
308
|
-
self.summary = QElidingLabel(parent=self)
|
|
309
|
-
self.summary.setObjectName('summary_text')
|
|
310
|
-
self.summary.setWordWrap(True)
|
|
311
|
-
|
|
312
|
-
font_summary = QFont()
|
|
313
|
-
font_summary.setPointSize(10)
|
|
314
|
-
self.summary.setFont(font_summary)
|
|
315
|
-
|
|
316
|
-
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
|
317
|
-
sizePolicy.setHorizontalStretch(1)
|
|
318
|
-
sizePolicy.setVerticalStretch(0)
|
|
319
|
-
self.summary.setSizePolicy(sizePolicy)
|
|
320
|
-
self.summary.setContentsMargins(0, -2, 0, -2)
|
|
321
|
-
|
|
322
|
-
# Package author
|
|
323
|
-
self.package_author = QElidingLabel(self)
|
|
324
|
-
self.package_author.setObjectName('author_text')
|
|
325
|
-
self.package_author.setWordWrap(True)
|
|
326
|
-
self.package_author.setSizePolicy(sizePolicy)
|
|
327
|
-
|
|
328
|
-
# Update button
|
|
329
|
-
self.update_btn = QPushButton('Update', self)
|
|
330
|
-
self.update_btn.setObjectName("install_button")
|
|
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)
|
|
337
|
-
|
|
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)
|
|
344
|
-
|
|
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
|
|
353
|
-
coll_icon = QColoredSVGIcon.from_resources('right_arrow').colored(
|
|
354
|
-
color='white',
|
|
355
|
-
)
|
|
356
|
-
exp_icon = QColoredSVGIcon.from_resources('down_arrow').colored(
|
|
357
|
-
color='white',
|
|
80
|
+
return QColoredSVGIcon.from_resources("warning").colored(
|
|
81
|
+
color="#E3B617"
|
|
358
82
|
)
|
|
359
|
-
self.install_info_button = QCollapsible(
|
|
360
|
-
"Installation Info", collapsedIcon=coll_icon, expandedIcon=exp_icon
|
|
361
|
-
)
|
|
362
|
-
self.install_info_button.setLayoutDirection(
|
|
363
|
-
Qt.RightToLeft
|
|
364
|
-
) # Make icon appear on the right
|
|
365
|
-
self.install_info_button.setObjectName("install_info_button")
|
|
366
|
-
self.install_info_button.setFixedWidth(180)
|
|
367
|
-
self.install_info_button.content().layout().setContentsMargins(
|
|
368
|
-
0, 0, 0, 0
|
|
369
|
-
)
|
|
370
|
-
self.install_info_button.content().setContentsMargins(0, 0, 0, 0)
|
|
371
|
-
self.install_info_button.content().layout().setSpacing(0)
|
|
372
|
-
self.install_info_button.layout().setContentsMargins(0, 0, 0, 0)
|
|
373
|
-
self.install_info_button.layout().setSpacing(2)
|
|
374
|
-
self.install_info_button.setSizePolicy(sizePolicy)
|
|
375
|
-
|
|
376
|
-
# Information widget for available packages
|
|
377
|
-
self.info_choice_wdg = QWidget(self)
|
|
378
|
-
self.info_choice_wdg.setObjectName('install_choice')
|
|
379
|
-
|
|
380
|
-
self.source_choice_text = QLabel('Source:')
|
|
381
|
-
self.version_choice_text = QLabel('Version:')
|
|
382
|
-
self.source_choice_dropdown = QComboBox()
|
|
383
|
-
self.version_choice_dropdown = QComboBox()
|
|
384
83
|
|
|
385
|
-
|
|
386
|
-
|
|
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)
|
|
394
|
-
self.source_choice_dropdown.currentTextChanged.connect(
|
|
395
|
-
self._populate_version_dropdown
|
|
84
|
+
def _collapsed_icon(self):
|
|
85
|
+
return QColoredSVGIcon.from_resources('right_arrow').colored(
|
|
86
|
+
color='white'
|
|
396
87
|
)
|
|
397
88
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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)
|
|
409
|
-
|
|
410
|
-
info_layout = QGridLayout()
|
|
411
|
-
info_layout.setContentsMargins(0, 0, 0, 0)
|
|
412
|
-
info_layout.setVerticalSpacing(0)
|
|
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)
|
|
418
|
-
|
|
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()
|
|
424
|
-
|
|
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
|
|
89
|
+
def _expanded_icon(self):
|
|
90
|
+
return QColoredSVGIcon.from_resources('down_arrow').colored(
|
|
91
|
+
color='white'
|
|
437
92
|
)
|
|
438
93
|
|
|
439
|
-
|
|
440
|
-
|
|
94
|
+
def _warning_tooltip(self):
|
|
95
|
+
return QtToolTipLabel(self)
|
|
441
96
|
|
|
442
|
-
|
|
443
|
-
|
|
97
|
+
def _trans(self, text, **kwargs):
|
|
98
|
+
return trans._(text, **kwargs)
|
|
444
99
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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()
|
|
100
|
+
def _handle_plugin_api_version(self, plugin_api_version):
|
|
101
|
+
if plugin_api_version in (None, 1):
|
|
102
|
+
return
|
|
452
103
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
)
|
|
459
|
-
layout_bottom.addWidget(
|
|
460
|
-
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'
|
|
461
109
|
)
|
|
462
|
-
|
|
463
|
-
|
|
110
|
+
icon = QColoredSVGIcon.from_resources('logo_silhouette').colored(
|
|
111
|
+
color='#33F0FF', opacity=opacity
|
|
464
112
|
)
|
|
465
|
-
|
|
466
|
-
self.action_button, alignment=Qt.AlignmentFlag.AlignTop
|
|
467
|
-
)
|
|
468
|
-
layout_bottom.addWidget(
|
|
469
|
-
self.cancel_btn, alignment=Qt.AlignmentFlag.AlignTop
|
|
470
|
-
)
|
|
471
|
-
|
|
472
|
-
info_layout = QGridLayout()
|
|
473
|
-
info_layout.setContentsMargins(0, 0, 0, 0)
|
|
474
|
-
info_layout.setVerticalSpacing(0)
|
|
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)
|
|
479
|
-
|
|
480
|
-
# endregion - Layout
|
|
481
|
-
|
|
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()
|
|
486
|
-
|
|
487
|
-
def _populate_version_dropdown(self, source: Literal["PyPI", "Conda"]):
|
|
488
|
-
"""Display the versions available after selecting a source: pypi or conda."""
|
|
489
|
-
if source == PYPI:
|
|
490
|
-
versions = self._versions_pypi
|
|
491
|
-
else:
|
|
492
|
-
versions = self._versions_conda
|
|
493
|
-
self.version_choice_dropdown.clear()
|
|
494
|
-
for version in versions:
|
|
495
|
-
self.version_choice_dropdown.addItem(version)
|
|
113
|
+
self.set_status(icon.pixmap(20, 20), text)
|
|
496
114
|
|
|
497
115
|
def _on_enabled_checkbox(self, state: int):
|
|
498
116
|
"""Called with `state` when checkbox is clicked."""
|
|
@@ -514,547 +132,79 @@ class PluginListItem(QFrame):
|
|
|
514
132
|
)
|
|
515
133
|
return
|
|
516
134
|
|
|
517
|
-
def
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
541
|
-
def show_warning(self, message: str = ""):
|
|
542
|
-
"""Show warning icon and tooltip."""
|
|
543
|
-
self.warning_tooltip.setVisible(bool(message))
|
|
544
|
-
self.warning_tooltip.setToolTip(message)
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
class QPluginList(QListWidget):
|
|
564
|
-
|
|
565
|
-
_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
|
|
566
139
|
|
|
567
|
-
def
|
|
568
|
-
|
|
569
|
-
self.installer = installer
|
|
570
|
-
self._remove_list = []
|
|
571
|
-
self._data = []
|
|
572
|
-
self._initial_height = None
|
|
573
|
-
|
|
574
|
-
self.setSortingEnabled(True)
|
|
575
|
-
|
|
576
|
-
def count_visible(self) -> int:
|
|
577
|
-
"""Return the number of visible items.
|
|
578
|
-
|
|
579
|
-
Visible items are the result of the normal `count` method minus
|
|
580
|
-
any hidden items.
|
|
581
|
-
"""
|
|
582
|
-
hidden = 0
|
|
583
|
-
count = self.count()
|
|
584
|
-
for i in range(count):
|
|
585
|
-
item = self.item(i)
|
|
586
|
-
hidden += item.isHidden()
|
|
587
|
-
|
|
588
|
-
return count - hidden
|
|
589
|
-
|
|
590
|
-
@Slot(tuple)
|
|
591
|
-
def addItem(
|
|
592
|
-
self,
|
|
593
|
-
project_info: ProjectInfoVersions,
|
|
594
|
-
installed=False,
|
|
595
|
-
plugin_name=None,
|
|
596
|
-
enabled=True,
|
|
597
|
-
npe_version=None,
|
|
598
|
-
):
|
|
599
|
-
pkg_name = project_info.metadata.name
|
|
600
|
-
# don't add duplicates
|
|
140
|
+
def _action_validation(self, tool, action):
|
|
141
|
+
global DISMISS_WARN_PYPI_INSTALL_DLG
|
|
601
142
|
if (
|
|
602
|
-
|
|
603
|
-
and
|
|
143
|
+
tool == InstallerTools.PIP
|
|
144
|
+
and action == InstallerActions.INSTALL
|
|
145
|
+
and self._warn_pypi_install()
|
|
146
|
+
and not DISMISS_WARN_PYPI_INSTALL_DLG
|
|
604
147
|
):
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
searchable_text = f"{pkg_name} {project_info.display_name} {project_info.metadata.summary}"
|
|
609
|
-
item = QListWidgetItem(searchable_text, self)
|
|
610
|
-
item.version = project_info.metadata.version
|
|
611
|
-
super().addItem(item)
|
|
612
|
-
widg = PluginListItem(
|
|
613
|
-
item=item,
|
|
614
|
-
package_name=pkg_name,
|
|
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,
|
|
621
|
-
parent=self,
|
|
622
|
-
plugin_name=plugin_name,
|
|
623
|
-
enabled=enabled,
|
|
624
|
-
installed=installed,
|
|
625
|
-
npe_version=npe_version,
|
|
626
|
-
versions_conda=project_info.conda_versions,
|
|
627
|
-
versions_pypi=project_info.pypi_versions,
|
|
628
|
-
)
|
|
629
|
-
item.widget = widg
|
|
630
|
-
item.npe_version = npe_version
|
|
631
|
-
item.setSizeHint(widg.sizeHint())
|
|
632
|
-
self.setItemWidget(item, widg)
|
|
633
|
-
|
|
634
|
-
if project_info.metadata.home_page:
|
|
635
|
-
widg.plugin_name.clicked.connect(
|
|
636
|
-
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')
|
|
637
151
|
)
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
widg.install_info_button.setDuration(0)
|
|
645
|
-
widg.install_info_button.toggled.connect(
|
|
646
|
-
lambda: self._resize_pluginlistitem(item)
|
|
647
|
-
)
|
|
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
|
-
|
|
672
|
-
def _resize_pluginlistitem(self, item):
|
|
673
|
-
"""Resize the plugin list item, especially after toggling QCollapsible."""
|
|
674
|
-
if item.widget.install_info_button.isExpanded():
|
|
675
|
-
item.widget.setFixedHeight(self._initial_height + 35)
|
|
676
|
-
else:
|
|
677
|
-
item.widget.setFixedHeight(self._initial_height)
|
|
678
|
-
|
|
679
|
-
item.setSizeHint(QSize(0, item.widget.height()))
|
|
680
|
-
|
|
681
|
-
def handle_action(
|
|
682
|
-
self,
|
|
683
|
-
item: QListWidgetItem,
|
|
684
|
-
pkg_name: str,
|
|
685
|
-
action_name: InstallerActions,
|
|
686
|
-
version: Optional[str] = None,
|
|
687
|
-
installer_choice: Optional[str] = None,
|
|
688
|
-
):
|
|
689
|
-
"""Determine which action is called (install, uninstall, update, cancel).
|
|
690
|
-
Update buttons appropriately and run the action."""
|
|
691
|
-
widget = item.widget
|
|
692
|
-
tool = installer_choice or widget.get_installer_tool()
|
|
693
|
-
self._remove_list.append((pkg_name, item))
|
|
694
|
-
self._warn_dialog = None
|
|
695
|
-
if not item.text().startswith(self._SORT_ORDER_PREFIX):
|
|
696
|
-
item.setText(f"{self._SORT_ORDER_PREFIX}{item.text()}")
|
|
697
|
-
|
|
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)
|
|
704
|
-
|
|
705
|
-
if action_name == InstallerActions.INSTALL:
|
|
706
|
-
if version:
|
|
707
|
-
pkg_name += (
|
|
708
|
-
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?'
|
|
709
158
|
)
|
|
710
|
-
widget.set_busy(trans._("installing..."), action_name)
|
|
711
|
-
|
|
712
|
-
job_id = self.installer.install(
|
|
713
|
-
tool=tool,
|
|
714
|
-
pkgs=[pkg_name],
|
|
715
|
-
# origins="TODO",
|
|
716
|
-
)
|
|
717
|
-
widget.setProperty("current_job_id", job_id)
|
|
718
|
-
if self._warn_dialog:
|
|
719
|
-
self._warn_dialog.exec_()
|
|
720
|
-
self.scrollToTop()
|
|
721
|
-
|
|
722
|
-
if action_name == InstallerActions.UPGRADE:
|
|
723
|
-
if hasattr(item, 'latest_version'):
|
|
724
|
-
pkg_name += f"=={item.latest_version}"
|
|
725
|
-
|
|
726
|
-
widget.set_busy(trans._("updating..."), action_name)
|
|
727
|
-
widget.update_btn.setDisabled(True)
|
|
728
|
-
widget.action_button.setDisabled(True)
|
|
729
|
-
|
|
730
|
-
job_id = self.installer.upgrade(
|
|
731
|
-
tool=tool,
|
|
732
|
-
pkgs=[pkg_name],
|
|
733
|
-
# origins="TODO",
|
|
734
|
-
)
|
|
735
|
-
widget.setProperty("current_job_id", job_id)
|
|
736
|
-
if self._warn_dialog:
|
|
737
|
-
self._warn_dialog.exec_()
|
|
738
|
-
self.scrollToTop()
|
|
739
|
-
|
|
740
|
-
elif action_name == InstallerActions.UNINSTALL:
|
|
741
|
-
widget.set_busy(trans._("uninstalling..."), action_name)
|
|
742
|
-
widget.update_btn.setDisabled(True)
|
|
743
|
-
job_id = self.installer.uninstall(
|
|
744
|
-
tool=tool,
|
|
745
|
-
pkgs=[pkg_name],
|
|
746
|
-
# origins="TODO",
|
|
747
|
-
# upgrade=False,
|
|
748
|
-
)
|
|
749
|
-
widget.setProperty("current_job_id", job_id)
|
|
750
|
-
if self._warn_dialog:
|
|
751
|
-
self._warn_dialog.exec_()
|
|
752
|
-
self.scrollToTop()
|
|
753
|
-
elif action_name == InstallerActions.CANCEL:
|
|
754
|
-
widget.set_busy(trans._("cancelling..."), action_name)
|
|
755
|
-
try:
|
|
756
|
-
job_id = widget.property("current_job_id")
|
|
757
|
-
self.installer.cancel(job_id)
|
|
758
|
-
finally:
|
|
759
|
-
widget.setProperty("current_job_id", None)
|
|
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
|
-
|
|
770
|
-
@Slot(npe2.PackageMetadata, bool)
|
|
771
|
-
def tag_outdated(self, metadata: npe2.PackageMetadata, is_available: bool):
|
|
772
|
-
"""Determines if an installed plugin is up to date with the latest version.
|
|
773
|
-
If it is not, the latest version will be displayed on the update button.
|
|
774
|
-
"""
|
|
775
|
-
if not is_available:
|
|
776
|
-
return
|
|
777
|
-
|
|
778
|
-
for item in self.findItems(
|
|
779
|
-
metadata.name, Qt.MatchFlag.MatchStartsWith
|
|
780
|
-
):
|
|
781
|
-
current = item.version
|
|
782
|
-
latest = metadata.version
|
|
783
|
-
is_marked_outdated = getattr(item, 'outdated', False)
|
|
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)
|
|
792
|
-
continue
|
|
793
|
-
if is_marked_outdated:
|
|
794
|
-
# already tagged it
|
|
795
|
-
continue
|
|
796
|
-
|
|
797
|
-
item.outdated = True
|
|
798
|
-
item.latest_version = latest
|
|
799
|
-
widg = self.itemWidget(item)
|
|
800
|
-
widg.update_btn.setVisible(True)
|
|
801
|
-
widg.update_btn.setText(
|
|
802
|
-
trans._("update (v{latest})", latest=latest)
|
|
803
159
|
)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
Tag list items as unavailable for install with conda-forge.
|
|
808
|
-
|
|
809
|
-
This will disable the item and the install button and add a warning
|
|
810
|
-
icon with a hover tooltip.
|
|
811
|
-
"""
|
|
812
|
-
for item in self.findItems(
|
|
813
|
-
metadata.name, Qt.MatchFlag.MatchStartsWith
|
|
814
|
-
):
|
|
815
|
-
widget = self.itemWidget(item)
|
|
816
|
-
widget.show_warning(
|
|
817
|
-
trans._(
|
|
818
|
-
"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"
|
|
819
163
|
)
|
|
820
164
|
)
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
def filter(self, text: str, starts_with_chars: int = 1):
|
|
828
|
-
"""Filter items to those containing `text`."""
|
|
829
|
-
if text:
|
|
830
|
-
# PySide has some issues, so we compare using id
|
|
831
|
-
# See: https://bugreports.qt.io/browse/PYSIDE-74
|
|
832
|
-
flag = (
|
|
833
|
-
Qt.MatchFlag.MatchStartsWith
|
|
834
|
-
if len(text) <= starts_with_chars
|
|
835
|
-
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
|
|
836
170
|
)
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
queries = (text,)
|
|
843
|
-
|
|
844
|
-
shown = {
|
|
845
|
-
id(it)
|
|
846
|
-
for query in queries
|
|
847
|
-
for it in self.findItems(query, flag)
|
|
848
|
-
}
|
|
849
|
-
for i in range(self.count()):
|
|
850
|
-
item = self.item(i)
|
|
851
|
-
item.setHidden(id(item) not in shown)
|
|
852
|
-
else:
|
|
853
|
-
for i in range(self.count()):
|
|
854
|
-
item = self.item(i)
|
|
855
|
-
item.setHidden(False)
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
class QtPluginDialog(QDialog):
|
|
859
|
-
def __init__(self, parent=None, prefix=None) -> None:
|
|
860
|
-
super().__init__(parent)
|
|
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
|
|
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
|
|
868
176
|
|
|
869
|
-
self.already_installed = set()
|
|
870
|
-
self.available_set = set()
|
|
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
177
|
|
|
880
|
-
|
|
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 = {}
|
|
885
|
-
self._add_items_timer = QTimer(self)
|
|
178
|
+
class QPluginList(BaseQPluginList):
|
|
886
179
|
|
|
887
|
-
|
|
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)
|
|
180
|
+
PLUGIN_LIST_ITEM_CLASS = PluginListItem
|
|
894
181
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
self._add_items_timer.timeout.connect(self._add_items)
|
|
898
|
-
|
|
899
|
-
self.installer = InstallerQueue(parent=self, prefix=prefix)
|
|
900
|
-
self.setWindowTitle(trans._('Plugin Manager'))
|
|
901
|
-
self._setup_ui()
|
|
902
|
-
self.installer.set_output_widget(self.stdout_text)
|
|
903
|
-
self.installer.started.connect(self._on_installer_start)
|
|
904
|
-
self.installer.processFinished.connect(self._on_process_finished)
|
|
905
|
-
self.installer.allFinished.connect(self._on_installer_all_finished)
|
|
906
|
-
self.setAcceptDrops(True)
|
|
182
|
+
def _trans(self, text, **kwargs):
|
|
183
|
+
return trans._(text, **kwargs)
|
|
907
184
|
|
|
185
|
+
def _before_handle_action(self, widget, action_name):
|
|
908
186
|
if (
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
# region - Private methods
|
|
915
|
-
# ------------------------------------------------------------------------
|
|
916
|
-
def _enable_refresh_button(self):
|
|
917
|
-
self.refresh_button.setEnabled(True)
|
|
187
|
+
widget.plugin_api_version != 1
|
|
188
|
+
and action_name == InstallerActions.UNINSTALL
|
|
189
|
+
):
|
|
190
|
+
_show_message(widget)
|
|
918
191
|
|
|
919
|
-
def _quit(self):
|
|
920
|
-
self.close()
|
|
921
|
-
with contextlib.suppress(AttributeError):
|
|
922
|
-
self._parent.close(quit_app=True, confirm_need=True)
|
|
923
192
|
|
|
924
|
-
|
|
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)
|
|
193
|
+
class QtPluginDialog(BaseQtPluginDialog):
|
|
929
194
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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'
|
|
935
200
|
|
|
936
|
-
|
|
937
|
-
QKeySequence(Qt.CTRL | Qt.Key_W), self
|
|
938
|
-
)
|
|
939
|
-
self._close_shortcut.activated.connect(self.close)
|
|
201
|
+
def _setup_theme_update(self):
|
|
940
202
|
get_settings().appearance.events.theme.connect(self._update_theme)
|
|
941
203
|
|
|
942
204
|
def _update_theme(self, event):
|
|
943
205
|
stylesheet = get_current_stylesheet([STYLES_PATH])
|
|
944
206
|
self.setStyleSheet(stylesheet)
|
|
945
207
|
|
|
946
|
-
def _on_installer_start(self):
|
|
947
|
-
"""Updates dialog buttons and status when installing a plugin."""
|
|
948
|
-
self.cancel_all_btn.setVisible(True)
|
|
949
|
-
self.working_indicator.show()
|
|
950
|
-
self.process_success_indicator.hide()
|
|
951
|
-
self.process_error_indicator.hide()
|
|
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()
|
|
996
|
-
|
|
997
|
-
self.working_indicator.hide()
|
|
998
|
-
if exit_code:
|
|
999
|
-
self.process_error_indicator.show()
|
|
1000
|
-
else:
|
|
1001
|
-
self.process_success_indicator.show()
|
|
1002
|
-
|
|
1003
|
-
def _on_installer_all_finished(self, exit_codes):
|
|
1004
|
-
self.working_indicator.hide()
|
|
1005
|
-
self.cancel_all_btn.setVisible(False)
|
|
1006
|
-
self.close_btn.setDisabled(False)
|
|
1007
|
-
self.refresh_button.setDisabled(False)
|
|
1008
|
-
|
|
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'))
|
|
1016
|
-
|
|
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()
|
|
1022
|
-
|
|
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)
|
|
1028
|
-
|
|
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', ''),
|
|
1048
|
-
),
|
|
1049
|
-
norm_name,
|
|
1050
|
-
[],
|
|
1051
|
-
[],
|
|
1052
|
-
),
|
|
1053
|
-
installed=True,
|
|
1054
|
-
enabled=enabled,
|
|
1055
|
-
npe_version=npe_version,
|
|
1056
|
-
)
|
|
1057
|
-
|
|
1058
208
|
def _add_installed(self, pkg_name=None):
|
|
1059
209
|
pm2 = npe2.PluginManager.instance()
|
|
1060
210
|
pm2.discover()
|
|
@@ -1066,7 +216,9 @@ class QtPluginDialog(QDialog):
|
|
|
1066
216
|
# if it's an Npe1 adaptor, call it v1
|
|
1067
217
|
npev = 'shim' if manifest.npe1_shim else 2
|
|
1068
218
|
if distname == pkg_name or pkg_name is None:
|
|
1069
|
-
self._add_to_installed(
|
|
219
|
+
self._add_to_installed(
|
|
220
|
+
distname, enabled, distname, plugin_api_version=npev
|
|
221
|
+
)
|
|
1070
222
|
|
|
1071
223
|
napari.plugins.plugin_manager.discover() # since they might not be loaded yet
|
|
1072
224
|
for (
|
|
@@ -1086,6 +238,7 @@ class QtPluginDialog(QDialog):
|
|
|
1086
238
|
self._add_to_installed(
|
|
1087
239
|
distname,
|
|
1088
240
|
not napari.plugins.plugin_manager.is_blocked(plugin_name),
|
|
241
|
+
normalized_name(distname or ''),
|
|
1089
242
|
)
|
|
1090
243
|
self._update_plugin_count()
|
|
1091
244
|
|
|
@@ -1095,7 +248,7 @@ class QtPluginDialog(QDialog):
|
|
|
1095
248
|
if widget.name == pkg_name:
|
|
1096
249
|
self.installed_list.scrollToItem(item)
|
|
1097
250
|
self.installed_list.setCurrentItem(item)
|
|
1098
|
-
if widget.
|
|
251
|
+
if widget.plugin_api_version != 1:
|
|
1099
252
|
_show_message(widget)
|
|
1100
253
|
break
|
|
1101
254
|
|
|
@@ -1109,436 +262,30 @@ class QtPluginDialog(QDialog):
|
|
|
1109
262
|
self.worker.yielded.connect(self._handle_yield)
|
|
1110
263
|
self.worker.started.connect(self.working_indicator.show)
|
|
1111
264
|
self.worker.finished.connect(self.working_indicator.hide)
|
|
1112
|
-
self.worker.finished.connect(self.
|
|
265
|
+
self.worker.finished.connect(self.finished)
|
|
266
|
+
self.worker.finished.connect(self.search)
|
|
1113
267
|
self.worker.start()
|
|
1114
268
|
|
|
1115
269
|
pm2 = npe2.PluginManager.instance()
|
|
1116
270
|
pm2.discover()
|
|
1117
271
|
|
|
1118
|
-
def
|
|
1119
|
-
"""Defines the layout for the PluginDialog."""
|
|
1120
|
-
self.resize(900, 600)
|
|
1121
|
-
vlay_1 = QVBoxLayout(self)
|
|
1122
|
-
self.h_splitter = QSplitter(self)
|
|
1123
|
-
vlay_1.addWidget(self.h_splitter)
|
|
1124
|
-
self.h_splitter.setOrientation(Qt.Orientation.Horizontal)
|
|
1125
|
-
self.v_splitter = QSplitter(self.h_splitter)
|
|
1126
|
-
self.v_splitter.setOrientation(Qt.Orientation.Vertical)
|
|
1127
|
-
self.v_splitter.setMinimumWidth(500)
|
|
1128
|
-
|
|
1129
|
-
installed = QWidget(self.v_splitter)
|
|
1130
|
-
lay = QVBoxLayout(installed)
|
|
1131
|
-
lay.setContentsMargins(0, 2, 0, 2)
|
|
1132
|
-
self.installed_label = QLabel(trans._("Installed Plugins"))
|
|
1133
|
-
self.packages_filter = QLineEdit()
|
|
1134
|
-
self.packages_filter.setPlaceholderText(trans._("filter..."))
|
|
1135
|
-
self.packages_filter.setMaximumWidth(350)
|
|
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
|
-
|
|
1148
|
-
mid_layout = QVBoxLayout()
|
|
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)
|
|
1155
|
-
mid_layout.addWidget(self.installed_label)
|
|
1156
|
-
lay.addLayout(mid_layout)
|
|
1157
|
-
|
|
1158
|
-
self.installed_list = QPluginList(installed, self.installer)
|
|
1159
|
-
lay.addWidget(self.installed_list)
|
|
1160
|
-
|
|
1161
|
-
uninstalled = QWidget(self.v_splitter)
|
|
1162
|
-
lay = QVBoxLayout(uninstalled)
|
|
1163
|
-
lay.setContentsMargins(0, 2, 0, 2)
|
|
1164
|
-
self.avail_label = QLabel(trans._("Available Plugins"))
|
|
1165
|
-
mid_layout = QHBoxLayout()
|
|
1166
|
-
mid_layout.addWidget(self.avail_label)
|
|
1167
|
-
mid_layout.addStretch()
|
|
1168
|
-
lay.addLayout(mid_layout)
|
|
1169
|
-
self.available_list = QPluginList(uninstalled, self.installer)
|
|
1170
|
-
lay.addWidget(self.available_list)
|
|
1171
|
-
|
|
1172
|
-
self.stdout_text = QTextEdit(self.v_splitter)
|
|
1173
|
-
self.stdout_text.setReadOnly(True)
|
|
1174
|
-
self.stdout_text.setObjectName("plugin_manager_process_status")
|
|
1175
|
-
self.stdout_text.hide()
|
|
1176
|
-
|
|
1177
|
-
buttonBox = QHBoxLayout()
|
|
1178
|
-
self.working_indicator = QLabel(trans._("loading ..."), self)
|
|
1179
|
-
sp = self.working_indicator.sizePolicy()
|
|
1180
|
-
sp.setRetainSizeWhenHidden(True)
|
|
1181
|
-
self.working_indicator.setSizePolicy(sp)
|
|
1182
|
-
self.process_error_indicator = QLabel(self)
|
|
1183
|
-
self.process_error_indicator.setObjectName("error_label")
|
|
1184
|
-
self.process_error_indicator.hide()
|
|
1185
|
-
self.process_success_indicator = QLabel(self)
|
|
1186
|
-
self.process_success_indicator.setObjectName("success_label")
|
|
1187
|
-
self.process_success_indicator.hide()
|
|
272
|
+
def _loading_gif(self):
|
|
1188
273
|
load_gif = str(Path(napari.resources.__file__).parent / "loading.gif")
|
|
1189
274
|
mov = QMovie(load_gif)
|
|
1190
275
|
mov.setScaledSize(QSize(18, 18))
|
|
1191
|
-
|
|
1192
|
-
mov.start()
|
|
1193
|
-
|
|
1194
|
-
visibility_direct_entry = not running_as_constructor_app()
|
|
1195
|
-
self.direct_entry_edit = QLineEdit(self)
|
|
1196
|
-
self.direct_entry_edit.installEventFilter(self)
|
|
1197
|
-
self.direct_entry_edit.returnPressed.connect(self._install_packages)
|
|
1198
|
-
self.direct_entry_edit.setVisible(visibility_direct_entry)
|
|
1199
|
-
self.direct_entry_btn = QToolButton(self)
|
|
1200
|
-
self.direct_entry_btn.setVisible(visibility_direct_entry)
|
|
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)
|
|
1225
|
-
|
|
1226
|
-
self.show_status_btn = QPushButton(trans._("Show Status"), self)
|
|
1227
|
-
self.show_status_btn.setFixedWidth(100)
|
|
1228
|
-
|
|
1229
|
-
self.cancel_all_btn = QPushButton(trans._("cancel all actions"), self)
|
|
1230
|
-
self.cancel_all_btn.setObjectName("remove_button")
|
|
1231
|
-
self.cancel_all_btn.setVisible(False)
|
|
1232
|
-
self.cancel_all_btn.clicked.connect(self.installer.cancel_all)
|
|
1233
|
-
|
|
1234
|
-
self.close_btn = QPushButton(trans._("Close"), self)
|
|
1235
|
-
self.close_btn.clicked.connect(self.accept)
|
|
1236
|
-
self.close_btn.setObjectName("close_button")
|
|
1237
|
-
|
|
1238
|
-
buttonBox.addWidget(self.show_status_btn)
|
|
1239
|
-
buttonBox.addWidget(self.working_indicator)
|
|
1240
|
-
buttonBox.addWidget(self.direct_entry_edit)
|
|
1241
|
-
buttonBox.addWidget(self.direct_entry_btn)
|
|
1242
|
-
if not visibility_direct_entry:
|
|
1243
|
-
buttonBox.addStretch()
|
|
1244
|
-
buttonBox.addWidget(self.process_success_indicator)
|
|
1245
|
-
buttonBox.addWidget(self.process_error_indicator)
|
|
1246
|
-
buttonBox.addSpacing(20)
|
|
1247
|
-
buttonBox.addWidget(self.cancel_all_btn)
|
|
1248
|
-
buttonBox.addSpacing(20)
|
|
1249
|
-
buttonBox.addWidget(self.close_btn)
|
|
1250
|
-
buttonBox.setContentsMargins(0, 0, 4, 0)
|
|
1251
|
-
vlay_1.addLayout(buttonBox)
|
|
1252
|
-
|
|
1253
|
-
self.show_status_btn.setCheckable(True)
|
|
1254
|
-
self.show_status_btn.setChecked(False)
|
|
1255
|
-
self.show_status_btn.toggled.connect(self.toggle_status)
|
|
1256
|
-
|
|
1257
|
-
self.v_splitter.setStretchFactor(1, 2)
|
|
1258
|
-
self.h_splitter.setStretchFactor(0, 2)
|
|
1259
|
-
|
|
1260
|
-
self.packages_filter.setFocus()
|
|
1261
|
-
self._update_direct_entry_text()
|
|
1262
|
-
|
|
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
|
-
)
|
|
1273
|
-
)
|
|
1274
|
-
|
|
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
|
-
)
|
|
1296
|
-
|
|
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
|
-
)
|
|
1307
|
-
else:
|
|
1308
|
-
self.avail_label.setText(
|
|
1309
|
-
trans._(
|
|
1310
|
-
"Available Plugins ({count}/{amount})",
|
|
1311
|
-
count=available_count_visible,
|
|
1312
|
-
amount=available_count,
|
|
1313
|
-
)
|
|
1314
|
-
)
|
|
1315
|
-
|
|
1316
|
-
def _install_packages(
|
|
1317
|
-
self,
|
|
1318
|
-
packages: Sequence[str] = (),
|
|
1319
|
-
):
|
|
1320
|
-
if not packages:
|
|
1321
|
-
_packages = self.direct_entry_edit.text()
|
|
1322
|
-
packages = (
|
|
1323
|
-
[_packages] if os.path.exists(_packages) else _packages.split()
|
|
1324
|
-
)
|
|
1325
|
-
self.direct_entry_edit.clear()
|
|
1326
|
-
|
|
1327
|
-
if packages:
|
|
1328
|
-
tool = (
|
|
1329
|
-
InstallerTools.CONDA
|
|
1330
|
-
if self._action_conda.isChecked()
|
|
1331
|
-
else InstallerTools.PIP
|
|
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
|
-
)
|
|
1344
|
-
|
|
1345
|
-
def _add_items(self):
|
|
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:
|
|
1351
|
-
if (
|
|
1352
|
-
self.installed_list.count() + self.available_list.count()
|
|
1353
|
-
== len(self._plugin_data)
|
|
1354
|
-
and self.available_list.count() != 0
|
|
1355
|
-
):
|
|
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
|
-
|
|
1364
|
-
return
|
|
1365
|
-
|
|
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
|
|
1374
|
-
)
|
|
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
|
|
1392
|
-
|
|
1393
|
-
if not self._filter_timer.isActive():
|
|
1394
|
-
self.filter(None, skip=True)
|
|
1395
|
-
|
|
1396
|
-
def _handle_yield(self, data: Tuple[npe2.PackageMetadata, bool, Dict]):
|
|
1397
|
-
"""Output from a worker process.
|
|
1398
|
-
|
|
1399
|
-
Includes information about the plugin, including available versions on conda and pypi.
|
|
1400
|
-
|
|
1401
|
-
The data is stored but the actual items are added via a timer in the `_add_items`
|
|
1402
|
-
method to prevent the UI from freezing by adding all items at once.
|
|
1403
|
-
"""
|
|
1404
|
-
self._plugin_data.append(data)
|
|
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
|
|
1450
|
-
|
|
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:
|
|
1475
|
-
"""Filter by text or set current text as filter."""
|
|
1476
|
-
if text is None:
|
|
1477
|
-
text = self.packages_filter.text()
|
|
1478
|
-
else:
|
|
1479
|
-
self.packages_filter.setText(text)
|
|
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
|
-
|
|
1493
|
-
self.installed_list.filter(text)
|
|
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()
|
|
276
|
+
return mov
|
|
1520
277
|
|
|
1521
|
-
def
|
|
1522
|
-
|
|
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()
|
|
278
|
+
def _on_bundle(self):
|
|
279
|
+
return running_as_constructor_app()
|
|
1529
280
|
|
|
1530
|
-
def
|
|
1531
|
-
|
|
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
|
|
281
|
+
def _show_info(self, info):
|
|
282
|
+
show_info(info)
|
|
1536
283
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
item.widget.prefix = prefix
|
|
284
|
+
def _show_warning(self, warning):
|
|
285
|
+
show_warning(warning)
|
|
1540
286
|
|
|
1541
|
-
|
|
287
|
+
def _trans(self, text, **kwargs):
|
|
288
|
+
return trans._(text, **kwargs)
|
|
1542
289
|
|
|
1543
290
|
|
|
1544
291
|
if __name__ == "__main__":
|