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.
@@ -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 QPoint, QSize, Qt, QTimer, Signal, Slot
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
- def _show_message(widget):
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(QFrame):
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
- # item, package_name, action_name, version, installer_choice
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
- self.warning_tooltip.setPixmap(
297
- icon.colored(color="#E3B617").pixmap(15, 15)
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
- # Update button
328
- self.update_btn = QPushButton('Update', self)
329
- self.update_btn.setObjectName("install_button")
330
- self.update_btn.setVisible(False)
331
- self.update_btn.clicked.connect(self._update_requested)
332
- sizePolicy.setRetainSizeWhenHidden(True)
333
- sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
334
- self.update_btn.setSizePolicy(sizePolicy)
335
- self.update_btn.clicked.connect(self._update_requested)
336
-
337
- # Action Button
338
- self.action_button = QPushButton(self)
339
- self.action_button.setFixedWidth(70)
340
- sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
341
- self.action_button.setSizePolicy(sizePolicy1)
342
- self.action_button.clicked.connect(self._action_requested)
343
-
344
- # Cancel
345
- self.cancel_btn = QPushButton("Cancel", self)
346
- self.cancel_btn.setObjectName("remove_button")
347
- self.cancel_btn.setSizePolicy(sizePolicy)
348
- self.cancel_btn.setFixedWidth(70)
349
- self.cancel_btn.clicked.connect(self._cancel_requested)
350
-
351
- # Collapsible button
352
- coll_icon = QColoredSVGIcon.from_resources('right_arrow').colored(
353
- color='white',
354
- )
355
- exp_icon = QColoredSVGIcon.from_resources('down_arrow').colored(
356
- color='white',
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
- # Error indicator
419
- self.error_indicator = QPushButton()
420
- self.error_indicator.setObjectName("warning_icon")
421
- self.error_indicator.setCursor(Qt.CursorShape.PointingHandCursor)
422
- self.error_indicator.hide()
423
-
424
- # region - Layout
425
- # -----------------------------------------------------------------
426
- layout = QHBoxLayout()
427
- layout.setSpacing(2)
428
- layout_left = QVBoxLayout()
429
- layout_right = QVBoxLayout()
430
- layout_top = QHBoxLayout()
431
- layout_bottom = QHBoxLayout()
432
- layout_bottom.setSpacing(4)
433
-
434
- layout_left.addWidget(
435
- self.enabled_checkbox, alignment=Qt.AlignmentFlag.AlignTop
70
+ def _expanded_icon(self):
71
+ return QColoredSVGIcon.from_resources('down_arrow').colored(
72
+ color='white'
436
73
  )
437
74
 
438
- layout_right.addLayout(layout_top, 1)
439
- layout_right.addLayout(layout_bottom, 100)
75
+ def _warning_tooltip(self):
76
+ return QtToolTipLabel(self)
440
77
 
441
- layout.addLayout(layout_left)
442
- layout.addLayout(layout_right)
78
+ def _trans(self, text, **kwargs):
79
+ return trans._(text, **kwargs)
443
80
 
444
- self.setLayout(layout)
445
-
446
- layout_top.addWidget(self.plugin_name)
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
- layout_bottom.addWidget(
453
- self.summary, alignment=Qt.AlignmentFlag.AlignTop, stretch=3
454
- )
455
- layout_bottom.addWidget(
456
- self.package_author, alignment=Qt.AlignmentFlag.AlignTop, stretch=1
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
- layout_bottom.addWidget(
468
- self.cancel_btn, alignment=Qt.AlignmentFlag.AlignTop
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 _cancel_requested(self):
517
- version = self.version_choice_dropdown.currentText()
518
- tool = self.get_installer_tool()
519
- self.actionRequested.emit(
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 _update_requested(self):
534
- version = self.version_choice_dropdown.currentText()
535
- tool = self.get_installer_tool()
536
- self.actionRequested.emit(
537
- self.item, self.name, InstallerActions.UPGRADE, version, tool
538
- )
539
-
540
- def show_warning(self, message: str = ""):
541
- """Show warning icon and tooltip."""
542
- self.warning_tooltip.setVisible(bool(message))
543
- self.warning_tooltip.setToolTip(message)
544
-
545
- def get_installer_source(self):
546
- return (
547
- CONDA
548
- if self.source_choice_dropdown.currentText() == CONDA
549
- or is_conda_package(self.name)
550
- else PYPI
551
- )
552
-
553
- def get_installer_tool(self):
554
- return (
555
- InstallerTools.CONDA
556
- if self.source_choice_dropdown.currentText() == CONDA
557
- or is_conda_package(self.name, prefix=self.prefix)
558
- else InstallerTools.PIP
559
- )
560
-
561
-
562
- class QPluginList(QListWidget):
563
-
564
- _SORT_ORDER_PREFIX = '0-'
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
- self.findItems(pkg_name, Qt.MatchFlag.MatchFixedString)
602
- and not plugin_name
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
- return
605
-
606
- # including summary here for sake of filtering below.
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
- widg.actionRequested.connect(self.handle_action)
639
- item.setSizeHint(item.widget.size())
640
- if self._initial_height is None:
641
- self._initial_height = item.widget.size().height()
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
- widget.setProperty("current_job_id", job_id)
749
- if self._warn_dialog:
750
- self._warn_dialog.exec_()
751
- self.scrollToTop()
752
- elif action_name == InstallerActions.CANCEL:
753
- widget.set_busy(trans._("cancelling..."), action_name)
754
- try:
755
- job_id = widget.property("current_job_id")
756
- self.installer.cancel(job_id)
757
- finally:
758
- widget.setProperty("current_job_id", None)
759
-
760
- def set_data(self, data):
761
- self._data = data
762
-
763
- def is_running(self):
764
- return self.count() != len(self._data)
765
-
766
- def packages(self):
767
- return [self.item(idx).widget.name for idx in range(self.count())]
768
-
769
- @Slot(npe2.PackageMetadata, bool)
770
- def tag_outdated(self, metadata: npe2.PackageMetadata, is_available: bool):
771
- """Determines if an installed plugin is up to date with the latest version.
772
- If it is not, the latest version will be displayed on the update button.
773
- """
774
- if not is_available:
775
- return
776
-
777
- for item in self.findItems(
778
- metadata.name, Qt.MatchFlag.MatchStartsWith
779
- ):
780
- current = item.version
781
- latest = metadata.version
782
- is_marked_outdated = getattr(item, 'outdated', False)
783
- if parse_version(current) >= parse_version(latest):
784
- # currently is up to date
785
- if is_marked_outdated:
786
- # previously marked as outdated, need to update item
787
- # `outdated` state and hide item widget `update_btn`
788
- item.outdated = False
789
- widg = self.itemWidget(item)
790
- widg.update_btn.setVisible(False)
791
- continue
792
- if is_marked_outdated:
793
- # already tagged it
794
- continue
795
-
796
- item.outdated = True
797
- item.latest_version = latest
798
- widg = self.itemWidget(item)
799
- widg.update_btn.setVisible(True)
800
- widg.update_btn.setText(
801
- trans._("update (v{latest})", latest=latest)
802
- )
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
- widget.setObjectName("unavailable")
821
- widget.style().unpolish(widget)
822
- widget.style().polish(widget)
823
- widget.action_button.setEnabled(False)
824
- widget.warning_tooltip.setVisible(True)
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
- if len(text) <= starts_with_chars:
837
- flag = Qt.MatchFlag.MatchStartsWith
838
- queries = (text, f'napari-{text}')
839
- else:
840
- flag = Qt.MatchFlag.MatchContains
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
- class QtPluginDialog(QDialog):
858
- def __init__(self, parent=None, prefix=None) -> None:
859
- super().__init__(parent)
161
+ PLUGIN_LIST_ITEM_CLASS = PluginListItem
860
162
 
861
- self._parent = parent
862
- if (
863
- parent is not None
864
- and getattr(parent, '_plugin_dialog', None) is None
865
- ):
866
- self._parent._plugin_dialog = self
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
- # region - Private methods
914
- # ------------------------------------------------------------------------
915
- def _enable_refresh_button(self):
916
- self.refresh_button.setEnabled(True)
167
+ class QtPluginDialog(BaseQtPluginDialog):
917
168
 
918
- def _quit(self):
919
- self.close()
920
- with contextlib.suppress(AttributeError):
921
- self._parent.close(quit_app=True, confirm_need=True)
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 _setup_shortcuts(self):
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._add_items_timer.start)
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 _setup_ui(self):
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
- self.working_indicator.setMovie(mov)
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 toggle_status(self, show=None):
1521
- show = not self.stdout_text.isVisible() if show is None else show
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 set_prefix(self, prefix):
1530
- self._prefix = prefix
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
- for idx in range(self.installed_list.count()):
1537
- item = self.installed_list.item(idx)
1538
- item.widget.prefix = prefix
264
+ def _show_warning(self, warning):
265
+ show_warning(warning)
1539
266
 
1540
- # endregion - Public methods
267
+ def _trans(self, text, **kwargs):
268
+ return trans._(text, **kwargs)
1541
269
 
1542
270
 
1543
271
  if __name__ == "__main__":