napari-plugin-manager 0.1.3__py3-none-any.whl → 0.1.5rc0__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 +70 -40
- napari_plugin_manager/_tests/test_npe2api.py +18 -10
- napari_plugin_manager/_tests/test_qt_plugin_dialog.py +209 -90
- napari_plugin_manager/_version.py +9 -4
- napari_plugin_manager/base_qt_package_installer.py +689 -0
- napari_plugin_manager/base_qt_plugin_dialog.py +1868 -0
- napari_plugin_manager/npe2api.py +1 -3
- napari_plugin_manager/qt_package_installer.py +31 -607
- napari_plugin_manager/qt_plugin_dialog.py +129 -1401
- napari_plugin_manager/qt_warning_dialog.py +19 -0
- napari_plugin_manager/utils.py +1 -2
- {napari_plugin_manager-0.1.3.dist-info → napari_plugin_manager-0.1.5rc0.dist-info}/METADATA +46 -11
- napari_plugin_manager-0.1.5rc0.dist-info/RECORD +23 -0
- {napari_plugin_manager-0.1.3.dist-info → napari_plugin_manager-0.1.5rc0.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.5rc0.dist-info/licenses}/LICENSE +0 -0
- {napari_plugin_manager-0.1.3.dist-info → napari_plugin_manager-0.1.5rc0.dist-info}/top_level.txt +0 -0
|
@@ -1,497 +1,97 @@
|
|
|
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
|
|
12
6
|
import npe2
|
|
13
7
|
from napari._qt.qt_resources import QColoredSVGIcon, get_current_stylesheet
|
|
14
8
|
from napari._qt.qthreading import create_worker
|
|
15
|
-
from napari._qt.widgets.qt_message_popup import WarnPopup
|
|
16
9
|
from napari._qt.widgets.qt_tooltip import QtToolTipLabel
|
|
17
10
|
from napari.plugins.utils import normalized_name
|
|
18
11
|
from napari.settings import get_settings
|
|
19
12
|
from napari.utils.misc import (
|
|
20
|
-
parse_version,
|
|
21
13
|
running_as_constructor_app,
|
|
22
14
|
)
|
|
23
15
|
from napari.utils.notifications import show_info, show_warning
|
|
24
16
|
from napari.utils.translations import trans
|
|
25
|
-
from qtpy.QtCore import
|
|
17
|
+
from qtpy.QtCore import QSize
|
|
26
18
|
from qtpy.QtGui import (
|
|
27
|
-
QAction,
|
|
28
|
-
QActionGroup,
|
|
29
|
-
QFont,
|
|
30
|
-
QKeySequence,
|
|
31
19
|
QMovie,
|
|
32
|
-
QShortcut,
|
|
33
20
|
)
|
|
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
|
|
21
|
+
from qtpy.QtWidgets import QCheckBox, QMessageBox
|
|
55
22
|
|
|
23
|
+
from napari_plugin_manager.base_qt_package_installer import (
|
|
24
|
+
InstallerActions,
|
|
25
|
+
InstallerTools,
|
|
26
|
+
)
|
|
27
|
+
from napari_plugin_manager.base_qt_plugin_dialog import (
|
|
28
|
+
BasePluginListItem,
|
|
29
|
+
BaseProjectInfoVersions,
|
|
30
|
+
BaseQPluginList,
|
|
31
|
+
BaseQtPluginDialog,
|
|
32
|
+
)
|
|
56
33
|
from napari_plugin_manager.npe2api import (
|
|
57
34
|
cache_clear,
|
|
58
35
|
iter_napari_plugin_info,
|
|
59
36
|
)
|
|
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
|
|
37
|
+
from napari_plugin_manager.qt_package_installer import NapariInstallerQueue
|
|
67
38
|
from napari_plugin_manager.utils import is_conda_package
|
|
68
39
|
|
|
69
40
|
# 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
41
|
STYLES_PATH = Path(__file__).parent / 'styles.qss'
|
|
42
|
+
DISMISS_WARN_PYPI_INSTALL_DLG = False
|
|
75
43
|
|
|
76
44
|
|
|
77
|
-
|
|
78
|
-
message = trans._(
|
|
79
|
-
'When installing/uninstalling npe2 plugins, '
|
|
80
|
-
'you must restart napari for UI changes to take effect.'
|
|
81
|
-
)
|
|
82
|
-
if widget.isVisible():
|
|
83
|
-
button = widget.action_button
|
|
84
|
-
warn_dialog = WarnPopup(text=message)
|
|
85
|
-
global_point = widget.action_button.mapToGlobal(
|
|
86
|
-
button.rect().topRight()
|
|
87
|
-
)
|
|
88
|
-
global_point = QPoint(
|
|
89
|
-
global_point.x() - button.width(), global_point.y()
|
|
90
|
-
)
|
|
91
|
-
warn_dialog.move(global_point)
|
|
92
|
-
warn_dialog.exec_()
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
class ProjectInfoVersions(NamedTuple):
|
|
45
|
+
class ProjectInfoVersions(BaseProjectInfoVersions):
|
|
96
46
|
metadata: npe2.PackageMetadata
|
|
97
|
-
display_name: str
|
|
98
|
-
pypi_versions: List[str]
|
|
99
|
-
conda_versions: List[str]
|
|
100
47
|
|
|
101
48
|
|
|
102
|
-
class PluginListItem(
|
|
49
|
+
class PluginListItem(BasePluginListItem):
|
|
103
50
|
"""An entry in the plugin dialog. This will include the package name, summary,
|
|
104
51
|
author, source, version, and buttons to update, install/uninstall, etc."""
|
|
105
52
|
|
|
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)
|
|
53
|
+
BASE_PACKAGE_NAME = 'napari'
|
|
290
54
|
|
|
55
|
+
def _warning_icon(self):
|
|
291
56
|
# TODO: This color should come from the theme but the theme needs
|
|
292
57
|
# to provide the right color. Default warning should be orange, not
|
|
293
58
|
# red. Code example:
|
|
294
59
|
# theme_name = get_settings().appearance.theme
|
|
295
60
|
# napari.utils.theme.get_theme(theme_name, as_dict=False).warning.as_hex()
|
|
296
|
-
|
|
297
|
-
|
|
61
|
+
return QColoredSVGIcon.from_resources("warning").colored(
|
|
62
|
+
color="#E3B617"
|
|
298
63
|
)
|
|
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
64
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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',
|
|
65
|
+
def _collapsed_icon(self):
|
|
66
|
+
return QColoredSVGIcon.from_resources('right_arrow').colored(
|
|
67
|
+
color='white'
|
|
357
68
|
)
|
|
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
|
-
|
|
384
|
-
if IS_NAPARI_CONDA_INSTALLED and self._versions_conda:
|
|
385
|
-
self.source_choice_dropdown.addItem(CONDA)
|
|
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
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
# Information widget for installed packages
|
|
398
|
-
self.info_widget = QWidget(self)
|
|
399
|
-
self.info_widget.setLayoutDirection(Qt.LeftToRight)
|
|
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
69
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
70
|
+
def _expanded_icon(self):
|
|
71
|
+
return QColoredSVGIcon.from_resources('down_arrow').colored(
|
|
72
|
+
color='white'
|
|
436
73
|
)
|
|
437
74
|
|
|
438
|
-
|
|
439
|
-
|
|
75
|
+
def _warning_tooltip(self):
|
|
76
|
+
return QtToolTipLabel(self)
|
|
440
77
|
|
|
441
|
-
|
|
442
|
-
|
|
78
|
+
def _trans(self, text, **kwargs):
|
|
79
|
+
return trans._(text, **kwargs)
|
|
443
80
|
|
|
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()
|
|
81
|
+
def _handle_plugin_api_version(self, plugin_api_version):
|
|
82
|
+
if plugin_api_version in (None, 1):
|
|
83
|
+
return
|
|
451
84
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
)
|
|
458
|
-
layout_bottom.addWidget(
|
|
459
|
-
self.update_btn, alignment=Qt.AlignmentFlag.AlignTop
|
|
460
|
-
)
|
|
461
|
-
layout_bottom.addWidget(
|
|
462
|
-
self.install_info_button, alignment=Qt.AlignmentFlag.AlignTop
|
|
463
|
-
)
|
|
464
|
-
layout_bottom.addWidget(
|
|
465
|
-
self.action_button, alignment=Qt.AlignmentFlag.AlignTop
|
|
85
|
+
opacity = 0.4 if plugin_api_version == 'shim' else 1
|
|
86
|
+
text = (
|
|
87
|
+
self._trans('npe1 (adapted)')
|
|
88
|
+
if plugin_api_version == 'shim'
|
|
89
|
+
else 'npe2'
|
|
466
90
|
)
|
|
467
|
-
|
|
468
|
-
|
|
91
|
+
icon = QColoredSVGIcon.from_resources('logo_silhouette').colored(
|
|
92
|
+
color='#33F0FF', opacity=opacity
|
|
469
93
|
)
|
|
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)
|
|
94
|
+
self.set_status(icon.pixmap(20, 20), text)
|
|
495
95
|
|
|
496
96
|
def _on_enabled_checkbox(self, state: int):
|
|
497
97
|
"""Called with `state` when checkbox is clicked."""
|
|
@@ -513,550 +113,76 @@ class PluginListItem(QFrame):
|
|
|
513
113
|
)
|
|
514
114
|
return
|
|
515
115
|
|
|
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)
|
|
116
|
+
def _warn_pypi_install(self):
|
|
117
|
+
return running_as_constructor_app() or is_conda_package(
|
|
118
|
+
'napari'
|
|
119
|
+
) # or True
|
|
532
120
|
|
|
533
|
-
def
|
|
534
|
-
|
|
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-'
|
|
565
|
-
|
|
566
|
-
def __init__(self, parent: QWidget, installer: InstallerQueue) -> None:
|
|
567
|
-
super().__init__(parent)
|
|
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
|
|
121
|
+
def _action_validation(self, tool, action):
|
|
122
|
+
global DISMISS_WARN_PYPI_INSTALL_DLG
|
|
600
123
|
if (
|
|
601
|
-
|
|
602
|
-
and
|
|
124
|
+
tool == InstallerTools.PIP
|
|
125
|
+
and action == InstallerActions.INSTALL
|
|
126
|
+
and self._warn_pypi_install()
|
|
127
|
+
and not DISMISS_WARN_PYPI_INSTALL_DLG
|
|
603
128
|
):
|
|
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)
|
|
129
|
+
warn_msgbox = QMessageBox(self)
|
|
130
|
+
warn_msgbox.setWindowTitle(
|
|
131
|
+
self._trans('PyPI installation on bundle/conda')
|
|
636
132
|
)
|
|
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()}"
|
|
133
|
+
warn_msgbox.setText(
|
|
134
|
+
self._trans(
|
|
135
|
+
'Installing from PyPI does not take into account existing installed packages, '
|
|
136
|
+
'so it can break existing installations. '
|
|
137
|
+
'If this happens the only solution is to reinstall the bundle/create a new conda environment.\n\n'
|
|
138
|
+
'Are you sure you want to install from PyPI?'
|
|
708
139
|
)
|
|
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
140
|
)
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
)
|
|
803
|
-
|
|
804
|
-
def tag_unavailable(self, metadata: npe2.PackageMetadata):
|
|
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"
|
|
141
|
+
warn_checkbox = QCheckBox(
|
|
142
|
+
self._trans(
|
|
143
|
+
"Don't show this message again in the current session"
|
|
818
144
|
)
|
|
819
145
|
)
|
|
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
|
|
146
|
+
warn_msgbox.setCheckBox(warn_checkbox)
|
|
147
|
+
warn_msgbox.setIcon(QMessageBox.Icon.Warning)
|
|
148
|
+
warn_msgbox.setStandardButtons(
|
|
149
|
+
QMessageBox.StandardButton.Ok
|
|
150
|
+
| QMessageBox.StandardButton.Cancel
|
|
835
151
|
)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
queries = (text,)
|
|
152
|
+
button_clicked = warn_msgbox.exec_()
|
|
153
|
+
DISMISS_WARN_PYPI_INSTALL_DLG = warn_checkbox.isChecked()
|
|
154
|
+
if button_clicked != QMessageBox.StandardButton.Ok:
|
|
155
|
+
return False
|
|
156
|
+
return True
|
|
842
157
|
|
|
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
158
|
|
|
159
|
+
class QPluginList(BaseQPluginList):
|
|
856
160
|
|
|
857
|
-
|
|
858
|
-
def __init__(self, parent=None, prefix=None) -> None:
|
|
859
|
-
super().__init__(parent)
|
|
161
|
+
PLUGIN_LIST_ITEM_CLASS = PluginListItem
|
|
860
162
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
parent is not None
|
|
864
|
-
and getattr(parent, '_plugin_dialog', None) is None
|
|
865
|
-
):
|
|
866
|
-
self._parent._plugin_dialog = self
|
|
867
|
-
|
|
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
|
-
|
|
879
|
-
# timer to avoid triggering a filter for every keystroke
|
|
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)
|
|
163
|
+
def _trans(self, text, **kwargs):
|
|
164
|
+
return trans._(text, **kwargs)
|
|
885
165
|
|
|
886
|
-
# Timer to avoid race conditions and incorrect count of plugins when
|
|
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)
|
|
893
|
-
|
|
894
|
-
# Add items in batches with a pause to avoid blocking the UI
|
|
895
|
-
self._add_items_timer.setInterval(61) # ms
|
|
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)
|
|
906
|
-
|
|
907
|
-
if (
|
|
908
|
-
parent is not None and parent._plugin_dialog is self
|
|
909
|
-
) or parent is None:
|
|
910
|
-
self.refresh()
|
|
911
|
-
self._setup_shortcuts()
|
|
912
166
|
|
|
913
|
-
|
|
914
|
-
# ------------------------------------------------------------------------
|
|
915
|
-
def _enable_refresh_button(self):
|
|
916
|
-
self.refresh_button.setEnabled(True)
|
|
167
|
+
class QtPluginDialog(BaseQtPluginDialog):
|
|
917
168
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
169
|
+
PACKAGE_METADATA_CLASS = npe2.PackageMetadata
|
|
170
|
+
PROJECT_INFO_VERSION_CLASS = ProjectInfoVersions
|
|
171
|
+
PLUGIN_LIST_CLASS = QPluginList
|
|
172
|
+
INSTALLER_QUEUE_CLASS = NapariInstallerQueue
|
|
173
|
+
BASE_PACKAGE_NAME = 'napari'
|
|
922
174
|
|
|
923
|
-
def
|
|
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)
|
|
928
|
-
|
|
929
|
-
self._quit_action = QAction(trans._('Exit'), self)
|
|
930
|
-
self._quit_action.setShortcut('Ctrl+Q')
|
|
931
|
-
self._quit_action.setMenuRole(QAction.QuitRole)
|
|
932
|
-
self._quit_action.triggered.connect(self._quit)
|
|
933
|
-
self.addAction(self._quit_action)
|
|
934
|
-
|
|
935
|
-
self._close_shortcut = QShortcut(
|
|
936
|
-
QKeySequence(Qt.CTRL | Qt.Key_W), self
|
|
937
|
-
)
|
|
938
|
-
self._close_shortcut.activated.connect(self.close)
|
|
175
|
+
def _setup_theme_update(self):
|
|
939
176
|
get_settings().appearance.events.theme.connect(self._update_theme)
|
|
940
177
|
|
|
941
178
|
def _update_theme(self, event):
|
|
942
179
|
stylesheet = get_current_stylesheet([STYLES_PATH])
|
|
943
180
|
self.setStyleSheet(stylesheet)
|
|
944
181
|
|
|
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
182
|
def _add_installed(self, pkg_name=None):
|
|
183
|
+
use_npe2_adaptor = get_settings().plugins.use_npe2_adaptor
|
|
1058
184
|
pm2 = npe2.PluginManager.instance()
|
|
1059
|
-
pm2.discover()
|
|
185
|
+
pm2.discover(include_npe1=use_npe2_adaptor)
|
|
1060
186
|
for manifest in pm2.iter_manifests():
|
|
1061
187
|
distname = normalized_name(manifest.name or '')
|
|
1062
188
|
if distname in self.already_installed or distname == 'napari':
|
|
@@ -1065,27 +191,36 @@ class QtPluginDialog(QDialog):
|
|
|
1065
191
|
# if it's an Npe1 adaptor, call it v1
|
|
1066
192
|
npev = 'shim' if manifest.npe1_shim else 2
|
|
1067
193
|
if distname == pkg_name or pkg_name is None:
|
|
1068
|
-
self._add_to_installed(distname, enabled, npe_version=npev)
|
|
1069
|
-
|
|
1070
|
-
napari.plugins.plugin_manager.discover() # since they might not be loaded yet
|
|
1071
|
-
for (
|
|
1072
|
-
plugin_name,
|
|
1073
|
-
_,
|
|
1074
|
-
distname,
|
|
1075
|
-
) in napari.plugins.plugin_manager.iter_available():
|
|
1076
|
-
# not showing these in the plugin dialog
|
|
1077
|
-
if plugin_name in (
|
|
1078
|
-
'napari_plugin_engine',
|
|
1079
|
-
'napari_plugin_manager',
|
|
1080
|
-
):
|
|
1081
|
-
continue
|
|
1082
|
-
if normalized_name(distname or '') in self.already_installed:
|
|
1083
|
-
continue
|
|
1084
|
-
if normalized_name(distname or '') == pkg_name or pkg_name is None:
|
|
1085
194
|
self._add_to_installed(
|
|
1086
|
-
distname,
|
|
1087
|
-
not napari.plugins.plugin_manager.is_blocked(plugin_name),
|
|
195
|
+
distname, enabled, distname, plugin_api_version=npev
|
|
1088
196
|
)
|
|
197
|
+
|
|
198
|
+
if not use_npe2_adaptor:
|
|
199
|
+
napari.plugins.plugin_manager.discover() # since they might not be loaded yet
|
|
200
|
+
for (
|
|
201
|
+
plugin_name,
|
|
202
|
+
_,
|
|
203
|
+
distname,
|
|
204
|
+
) in napari.plugins.plugin_manager.iter_available():
|
|
205
|
+
# not showing these in the plugin dialog
|
|
206
|
+
if plugin_name in (
|
|
207
|
+
'napari_plugin_engine',
|
|
208
|
+
'napari_plugin_manager',
|
|
209
|
+
):
|
|
210
|
+
continue
|
|
211
|
+
if normalized_name(distname or '') in self.already_installed:
|
|
212
|
+
continue
|
|
213
|
+
if (
|
|
214
|
+
normalized_name(distname or '') == pkg_name
|
|
215
|
+
or pkg_name is None
|
|
216
|
+
):
|
|
217
|
+
self._add_to_installed(
|
|
218
|
+
distname,
|
|
219
|
+
not napari.plugins.plugin_manager.is_blocked(
|
|
220
|
+
plugin_name
|
|
221
|
+
),
|
|
222
|
+
normalized_name(distname or ''),
|
|
223
|
+
)
|
|
1089
224
|
self._update_plugin_count()
|
|
1090
225
|
|
|
1091
226
|
for i in range(self.installed_list.count()):
|
|
@@ -1094,12 +229,11 @@ class QtPluginDialog(QDialog):
|
|
|
1094
229
|
if widget.name == pkg_name:
|
|
1095
230
|
self.installed_list.scrollToItem(item)
|
|
1096
231
|
self.installed_list.setCurrentItem(item)
|
|
1097
|
-
if widget.npe_version != 1:
|
|
1098
|
-
_show_message(widget)
|
|
1099
232
|
break
|
|
1100
233
|
|
|
1101
234
|
def _fetch_available_plugins(self, clear_cache: bool = False):
|
|
1102
|
-
get_settings()
|
|
235
|
+
settings = get_settings()
|
|
236
|
+
use_npe2_adaptor = settings.plugins.use_npe2_adaptor
|
|
1103
237
|
|
|
1104
238
|
if clear_cache:
|
|
1105
239
|
cache_clear()
|
|
@@ -1108,436 +242,30 @@ class QtPluginDialog(QDialog):
|
|
|
1108
242
|
self.worker.yielded.connect(self._handle_yield)
|
|
1109
243
|
self.worker.started.connect(self.working_indicator.show)
|
|
1110
244
|
self.worker.finished.connect(self.working_indicator.hide)
|
|
1111
|
-
self.worker.finished.connect(self.
|
|
245
|
+
self.worker.finished.connect(self.finished)
|
|
246
|
+
self.worker.finished.connect(self.search)
|
|
1112
247
|
self.worker.start()
|
|
1113
248
|
|
|
1114
249
|
pm2 = npe2.PluginManager.instance()
|
|
1115
|
-
pm2.discover()
|
|
250
|
+
pm2.discover(include_npe1=use_npe2_adaptor)
|
|
1116
251
|
|
|
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()
|
|
252
|
+
def _loading_gif(self):
|
|
1187
253
|
load_gif = str(Path(napari.resources.__file__).parent / "loading.gif")
|
|
1188
254
|
mov = QMovie(load_gif)
|
|
1189
255
|
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()
|
|
256
|
+
return mov
|
|
1519
257
|
|
|
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()
|
|
258
|
+
def _on_bundle(self):
|
|
259
|
+
return running_as_constructor_app()
|
|
1528
260
|
|
|
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
|
|
261
|
+
def _show_info(self, info):
|
|
262
|
+
show_info(info)
|
|
1535
263
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
item.widget.prefix = prefix
|
|
264
|
+
def _show_warning(self, warning):
|
|
265
|
+
show_warning(warning)
|
|
1539
266
|
|
|
1540
|
-
|
|
267
|
+
def _trans(self, text, **kwargs):
|
|
268
|
+
return trans._(text, **kwargs)
|
|
1541
269
|
|
|
1542
270
|
|
|
1543
271
|
if __name__ == "__main__":
|