napari-plugin-manager 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

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