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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,5 @@
1
- import contextlib
2
- import importlib.metadata
3
- import os
4
1
  import sys
5
- import webbrowser
6
- from functools import partial
7
2
  from pathlib import Path
8
- from typing import Dict, List, Literal, NamedTuple, Optional, Sequence, Tuple
9
3
 
10
4
  import napari.plugins
11
5
  import napari.resources
@@ -17,61 +11,36 @@ from napari._qt.widgets.qt_tooltip import QtToolTipLabel
17
11
  from napari.plugins.utils import normalized_name
18
12
  from napari.settings import get_settings
19
13
  from napari.utils.misc import (
20
- parse_version,
21
14
  running_as_constructor_app,
22
15
  )
23
16
  from napari.utils.notifications import show_info, show_warning
24
17
  from napari.utils.translations import trans
25
- from qtpy.QtCore import QPoint, QSize, Qt, QTimer, Signal, Slot
18
+ from qtpy.QtCore import QPoint, QSize
26
19
  from qtpy.QtGui import (
27
- QAction,
28
- QActionGroup,
29
- QFont,
30
- QKeySequence,
31
20
  QMovie,
32
- QShortcut,
33
21
  )
34
- from qtpy.QtWidgets import (
35
- QCheckBox,
36
- QComboBox,
37
- QDialog,
38
- QFrame,
39
- QGridLayout,
40
- QHBoxLayout,
41
- QLabel,
42
- QLineEdit,
43
- QListWidget,
44
- QListWidgetItem,
45
- QMenu,
46
- QPushButton,
47
- QSizePolicy,
48
- QSplitter,
49
- QTextEdit,
50
- QToolButton,
51
- QVBoxLayout,
52
- QWidget,
53
- )
54
- from superqt import QCollapsible, QElidingLabel
22
+ from qtpy.QtWidgets import QCheckBox, QMessageBox
55
23
 
24
+ from napari_plugin_manager.base_qt_package_installer import (
25
+ InstallerActions,
26
+ InstallerTools,
27
+ )
28
+ from napari_plugin_manager.base_qt_plugin_dialog import (
29
+ BasePluginListItem,
30
+ BaseProjectInfoVersions,
31
+ BaseQPluginList,
32
+ BaseQtPluginDialog,
33
+ )
56
34
  from napari_plugin_manager.npe2api import (
57
35
  cache_clear,
58
36
  iter_napari_plugin_info,
59
37
  )
60
- from napari_plugin_manager.qt_package_installer import (
61
- InstallerActions,
62
- InstallerQueue,
63
- InstallerTools,
64
- ProcessFinishedData,
65
- )
66
- from napari_plugin_manager.qt_widgets import ClickableLabel
38
+ from napari_plugin_manager.qt_package_installer import NapariInstallerQueue
67
39
  from napari_plugin_manager.utils import is_conda_package
68
40
 
69
41
  # Scaling factor for each list widget item when expanding.
70
- CONDA = 'Conda'
71
- PYPI = 'PyPI'
72
- ON_BUNDLE = running_as_constructor_app()
73
- IS_NAPARI_CONDA_INSTALLED = is_conda_package('napari')
74
42
  STYLES_PATH = Path(__file__).parent / 'styles.qss'
43
+ DISMISS_WARN_PYPI_INSTALL_DLG = False
75
44
 
76
45
 
77
46
  def _show_message(widget):
@@ -92,406 +61,56 @@ def _show_message(widget):
92
61
  warn_dialog.exec_()
93
62
 
94
63
 
95
- class ProjectInfoVersions(NamedTuple):
64
+ class ProjectInfoVersions(BaseProjectInfoVersions):
96
65
  metadata: npe2.PackageMetadata
97
- display_name: str
98
- pypi_versions: List[str]
99
- conda_versions: List[str]
100
66
 
101
67
 
102
- class PluginListItem(QFrame):
68
+ class PluginListItem(BasePluginListItem):
103
69
  """An entry in the plugin dialog. This will include the package name, summary,
104
70
  author, source, version, and buttons to update, install/uninstall, etc."""
105
71
 
106
- # 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)
72
+ BASE_PACKAGE_NAME = 'napari'
290
73
 
74
+ def _warning_icon(self):
291
75
  # TODO: This color should come from the theme but the theme needs
292
76
  # to provide the right color. Default warning should be orange, not
293
77
  # red. Code example:
294
78
  # theme_name = get_settings().appearance.theme
295
79
  # napari.utils.theme.get_theme(theme_name, as_dict=False).warning.as_hex()
296
- self.warning_tooltip.setPixmap(
297
- icon.colored(color="#E3B617").pixmap(15, 15)
298
- )
299
- self.warning_tooltip.setVisible(False)
300
-
301
- # Item status
302
- self.item_status = QLabel(self)
303
- self.item_status.setObjectName("small_italic_text")
304
- self.item_status.setSizePolicy(sizePolicy)
305
-
306
- # Summary
307
- self.summary = QElidingLabel(parent=self)
308
- self.summary.setObjectName('summary_text')
309
- self.summary.setWordWrap(True)
310
-
311
- font_summary = QFont()
312
- font_summary.setPointSize(10)
313
- self.summary.setFont(font_summary)
314
-
315
- sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
316
- sizePolicy.setHorizontalStretch(1)
317
- sizePolicy.setVerticalStretch(0)
318
- self.summary.setSizePolicy(sizePolicy)
319
- self.summary.setContentsMargins(0, -2, 0, -2)
320
-
321
- # Package author
322
- self.package_author = QElidingLabel(self)
323
- self.package_author.setObjectName('author_text')
324
- self.package_author.setWordWrap(True)
325
- self.package_author.setSizePolicy(sizePolicy)
326
-
327
- # Update button
328
- self.update_btn = QPushButton('Update', self)
329
- self.update_btn.setObjectName("install_button")
330
- self.update_btn.setVisible(False)
331
- self.update_btn.clicked.connect(self._update_requested)
332
- sizePolicy.setRetainSizeWhenHidden(True)
333
- sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
334
- self.update_btn.setSizePolicy(sizePolicy)
335
- self.update_btn.clicked.connect(self._update_requested)
336
-
337
- # Action Button
338
- self.action_button = QPushButton(self)
339
- self.action_button.setFixedWidth(70)
340
- sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
341
- self.action_button.setSizePolicy(sizePolicy1)
342
- self.action_button.clicked.connect(self._action_requested)
343
-
344
- # Cancel
345
- self.cancel_btn = QPushButton("Cancel", self)
346
- self.cancel_btn.setObjectName("remove_button")
347
- self.cancel_btn.setSizePolicy(sizePolicy)
348
- self.cancel_btn.setFixedWidth(70)
349
- self.cancel_btn.clicked.connect(self._cancel_requested)
350
-
351
- # Collapsible button
352
- coll_icon = QColoredSVGIcon.from_resources('right_arrow').colored(
353
- color='white',
354
- )
355
- exp_icon = QColoredSVGIcon.from_resources('down_arrow').colored(
356
- color='white',
80
+ return QColoredSVGIcon.from_resources("warning").colored(
81
+ color="#E3B617"
357
82
  )
358
- self.install_info_button = QCollapsible(
359
- "Installation Info", collapsedIcon=coll_icon, expandedIcon=exp_icon
360
- )
361
- self.install_info_button.setLayoutDirection(
362
- Qt.RightToLeft
363
- ) # Make icon appear on the right
364
- self.install_info_button.setObjectName("install_info_button")
365
- self.install_info_button.setFixedWidth(180)
366
- self.install_info_button.content().layout().setContentsMargins(
367
- 0, 0, 0, 0
368
- )
369
- self.install_info_button.content().setContentsMargins(0, 0, 0, 0)
370
- self.install_info_button.content().layout().setSpacing(0)
371
- self.install_info_button.layout().setContentsMargins(0, 0, 0, 0)
372
- self.install_info_button.layout().setSpacing(2)
373
- self.install_info_button.setSizePolicy(sizePolicy)
374
-
375
- # Information widget for available packages
376
- self.info_choice_wdg = QWidget(self)
377
- self.info_choice_wdg.setObjectName('install_choice')
378
-
379
- self.source_choice_text = QLabel('Source:')
380
- self.version_choice_text = QLabel('Version:')
381
- self.source_choice_dropdown = QComboBox()
382
- self.version_choice_dropdown = QComboBox()
383
83
 
384
- 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
84
+ def _collapsed_icon(self):
85
+ return QColoredSVGIcon.from_resources('right_arrow').colored(
86
+ color='white'
395
87
  )
396
88
 
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
-
418
- # Error indicator
419
- self.error_indicator = QPushButton()
420
- self.error_indicator.setObjectName("warning_icon")
421
- self.error_indicator.setCursor(Qt.CursorShape.PointingHandCursor)
422
- self.error_indicator.hide()
423
-
424
- # region - Layout
425
- # -----------------------------------------------------------------
426
- layout = QHBoxLayout()
427
- layout.setSpacing(2)
428
- layout_left = QVBoxLayout()
429
- layout_right = QVBoxLayout()
430
- layout_top = QHBoxLayout()
431
- layout_bottom = QHBoxLayout()
432
- layout_bottom.setSpacing(4)
433
-
434
- layout_left.addWidget(
435
- self.enabled_checkbox, alignment=Qt.AlignmentFlag.AlignTop
89
+ def _expanded_icon(self):
90
+ return QColoredSVGIcon.from_resources('down_arrow').colored(
91
+ color='white'
436
92
  )
437
93
 
438
- layout_right.addLayout(layout_top, 1)
439
- layout_right.addLayout(layout_bottom, 100)
94
+ def _warning_tooltip(self):
95
+ return QtToolTipLabel(self)
440
96
 
441
- layout.addLayout(layout_left)
442
- layout.addLayout(layout_right)
97
+ def _trans(self, text, **kwargs):
98
+ return trans._(text, **kwargs)
443
99
 
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()
100
+ def _handle_plugin_api_version(self, plugin_api_version):
101
+ if plugin_api_version in (None, 1):
102
+ return
451
103
 
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
104
+ opacity = 0.4 if plugin_api_version == 'shim' else 1
105
+ text = (
106
+ self._trans('npe1 (adapted)')
107
+ if plugin_api_version == 'shim'
108
+ else 'npe2'
460
109
  )
461
- layout_bottom.addWidget(
462
- self.install_info_button, alignment=Qt.AlignmentFlag.AlignTop
110
+ icon = QColoredSVGIcon.from_resources('logo_silhouette').colored(
111
+ color='#33F0FF', opacity=opacity
463
112
  )
464
- layout_bottom.addWidget(
465
- self.action_button, alignment=Qt.AlignmentFlag.AlignTop
466
- )
467
- layout_bottom.addWidget(
468
- self.cancel_btn, alignment=Qt.AlignmentFlag.AlignTop
469
- )
470
-
471
- info_layout = QGridLayout()
472
- info_layout.setContentsMargins(0, 0, 0, 0)
473
- info_layout.setVerticalSpacing(0)
474
- info_layout.addWidget(self.source_choice_text, 0, 0, 1, 1)
475
- info_layout.addWidget(self.source_choice_dropdown, 1, 0, 1, 1)
476
- info_layout.addWidget(self.version_choice_text, 0, 1, 1, 1)
477
- info_layout.addWidget(self.version_choice_dropdown, 1, 1, 1, 1)
478
-
479
- # endregion - Layout
480
-
481
- self.info_choice_wdg.setLayout(info_layout)
482
- self.info_choice_wdg.setLayoutDirection(Qt.LeftToRight)
483
- self.info_choice_wdg.setObjectName("install_choice_widget")
484
- self.info_choice_wdg.hide()
485
-
486
- def _populate_version_dropdown(self, source: Literal["PyPI", "Conda"]):
487
- """Display the versions available after selecting a source: pypi or conda."""
488
- if source == PYPI:
489
- versions = self._versions_pypi
490
- else:
491
- versions = self._versions_conda
492
- self.version_choice_dropdown.clear()
493
- for version in versions:
494
- self.version_choice_dropdown.addItem(version)
113
+ self.set_status(icon.pixmap(20, 20), text)
495
114
 
496
115
  def _on_enabled_checkbox(self, state: int):
497
116
  """Called with `state` when checkbox is clicked."""
@@ -513,547 +132,79 @@ class PluginListItem(QFrame):
513
132
  )
514
133
  return
515
134
 
516
- def _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)
532
-
533
- def _update_requested(self):
534
- version = self.version_choice_dropdown.currentText()
535
- tool = self.get_installer_tool()
536
- self.actionRequested.emit(
537
- self.item, self.name, InstallerActions.UPGRADE, version, tool
538
- )
539
-
540
- def show_warning(self, message: str = ""):
541
- """Show warning icon and tooltip."""
542
- self.warning_tooltip.setVisible(bool(message))
543
- self.warning_tooltip.setToolTip(message)
544
-
545
- def get_installer_source(self):
546
- return (
547
- CONDA
548
- if self.source_choice_dropdown.currentText() == CONDA
549
- or is_conda_package(self.name)
550
- else PYPI
551
- )
552
-
553
- def get_installer_tool(self):
554
- return (
555
- InstallerTools.CONDA
556
- if self.source_choice_dropdown.currentText() == CONDA
557
- or is_conda_package(self.name, prefix=self.prefix)
558
- else InstallerTools.PIP
559
- )
560
-
561
-
562
- class QPluginList(QListWidget):
563
-
564
- _SORT_ORDER_PREFIX = '0-'
135
+ def _warn_pypi_install(self):
136
+ return running_as_constructor_app() or is_conda_package(
137
+ 'napari'
138
+ ) # or True
565
139
 
566
- def __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
140
+ def _action_validation(self, tool, action):
141
+ global DISMISS_WARN_PYPI_INSTALL_DLG
600
142
  if (
601
- self.findItems(pkg_name, Qt.MatchFlag.MatchFixedString)
602
- 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
603
147
  ):
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)
148
+ warn_msgbox = QMessageBox(self)
149
+ warn_msgbox.setWindowTitle(
150
+ self._trans('PyPI installation on bundle/conda')
636
151
  )
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()}"
152
+ warn_msgbox.setText(
153
+ self._trans(
154
+ 'Installing from PyPI does not take into account existing installed packages, '
155
+ 'so it can break existing installations. '
156
+ 'If this happens the only solution is to reinstall the bundle/create a new conda environment.\n\n'
157
+ 'Are you sure you want to install from PyPI?'
708
158
  )
709
- widget.set_busy(trans._("installing..."), action_name)
710
-
711
- job_id = self.installer.install(
712
- tool=tool,
713
- pkgs=[pkg_name],
714
- # origins="TODO",
715
- )
716
- widget.setProperty("current_job_id", job_id)
717
- if self._warn_dialog:
718
- self._warn_dialog.exec_()
719
- self.scrollToTop()
720
-
721
- if action_name == InstallerActions.UPGRADE:
722
- if hasattr(item, 'latest_version'):
723
- pkg_name += f"=={item.latest_version}"
724
-
725
- widget.set_busy(trans._("updating..."), action_name)
726
- widget.update_btn.setDisabled(True)
727
- widget.action_button.setDisabled(True)
728
-
729
- job_id = self.installer.upgrade(
730
- tool=tool,
731
- pkgs=[pkg_name],
732
- # origins="TODO",
733
- )
734
- widget.setProperty("current_job_id", job_id)
735
- if self._warn_dialog:
736
- self._warn_dialog.exec_()
737
- self.scrollToTop()
738
-
739
- elif action_name == InstallerActions.UNINSTALL:
740
- widget.set_busy(trans._("uninstalling..."), action_name)
741
- widget.update_btn.setDisabled(True)
742
- job_id = self.installer.uninstall(
743
- tool=tool,
744
- pkgs=[pkg_name],
745
- # origins="TODO",
746
- # upgrade=False,
747
- )
748
- widget.setProperty("current_job_id", job_id)
749
- if self._warn_dialog:
750
- self._warn_dialog.exec_()
751
- self.scrollToTop()
752
- elif action_name == InstallerActions.CANCEL:
753
- widget.set_busy(trans._("cancelling..."), action_name)
754
- try:
755
- job_id = widget.property("current_job_id")
756
- self.installer.cancel(job_id)
757
- finally:
758
- widget.setProperty("current_job_id", None)
759
-
760
- def set_data(self, data):
761
- self._data = data
762
-
763
- def is_running(self):
764
- return self.count() != len(self._data)
765
-
766
- def packages(self):
767
- return [self.item(idx).widget.name for idx in range(self.count())]
768
-
769
- @Slot(npe2.PackageMetadata, bool)
770
- def tag_outdated(self, metadata: npe2.PackageMetadata, is_available: bool):
771
- """Determines if an installed plugin is up to date with the latest version.
772
- If it is not, the latest version will be displayed on the update button.
773
- """
774
- if not is_available:
775
- return
776
-
777
- for item in self.findItems(
778
- metadata.name, Qt.MatchFlag.MatchStartsWith
779
- ):
780
- current = item.version
781
- latest = metadata.version
782
- is_marked_outdated = getattr(item, 'outdated', False)
783
- if parse_version(current) >= parse_version(latest):
784
- # currently is up to date
785
- if is_marked_outdated:
786
- # previously marked as outdated, need to update item
787
- # `outdated` state and hide item widget `update_btn`
788
- item.outdated = False
789
- widg = self.itemWidget(item)
790
- widg.update_btn.setVisible(False)
791
- continue
792
- if is_marked_outdated:
793
- # already tagged it
794
- continue
795
-
796
- item.outdated = True
797
- item.latest_version = latest
798
- widg = self.itemWidget(item)
799
- widg.update_btn.setVisible(True)
800
- widg.update_btn.setText(
801
- trans._("update (v{latest})", latest=latest)
802
159
  )
803
-
804
- 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"
160
+ warn_checkbox = QCheckBox(
161
+ self._trans(
162
+ "Don't show this message again in the current session"
818
163
  )
819
164
  )
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
165
+ warn_msgbox.setCheckBox(warn_checkbox)
166
+ warn_msgbox.setIcon(QMessageBox.Icon.Warning)
167
+ warn_msgbox.setStandardButtons(
168
+ QMessageBox.StandardButton.Ok
169
+ | QMessageBox.StandardButton.Cancel
835
170
  )
836
- 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,)
842
-
843
- shown = {
844
- id(it)
845
- for query in queries
846
- for it in self.findItems(query, flag)
847
- }
848
- for i in range(self.count()):
849
- item = self.item(i)
850
- item.setHidden(id(item) not in shown)
851
- else:
852
- for i in range(self.count()):
853
- item = self.item(i)
854
- item.setHidden(False)
855
-
856
-
857
- class QtPluginDialog(QDialog):
858
- def __init__(self, parent=None, prefix=None) -> None:
859
- super().__init__(parent)
860
-
861
- self._parent = parent
862
- if (
863
- parent is not None
864
- and getattr(parent, '_plugin_dialog', None) is None
865
- ):
866
- self._parent._plugin_dialog = self
171
+ button_clicked = warn_msgbox.exec_()
172
+ DISMISS_WARN_PYPI_INSTALL_DLG = warn_checkbox.isChecked()
173
+ if button_clicked != QMessageBox.StandardButton.Ok:
174
+ return False
175
+ return True
867
176
 
868
- self.already_installed = set()
869
- self.available_set = set()
870
- self._prefix = prefix
871
- self._first_open = True
872
- self._plugin_queue = [] # Store plugin data to be added
873
- self._plugin_data = [] # Store all plugin data
874
- self._filter_texts = []
875
- self._filter_idxs_cache = set()
876
- self._filter_timer = QTimer(self)
877
- self.worker = None
878
177
 
879
- # 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)
178
+ class QPluginList(BaseQPluginList):
885
179
 
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)
180
+ PLUGIN_LIST_ITEM_CLASS = PluginListItem
893
181
 
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)
182
+ def _trans(self, text, **kwargs):
183
+ return trans._(text, **kwargs)
906
184
 
185
+ def _before_handle_action(self, widget, action_name):
907
186
  if (
908
- parent is not None and parent._plugin_dialog is self
909
- ) or parent is None:
910
- self.refresh()
911
- self._setup_shortcuts()
912
-
913
- # region - Private methods
914
- # ------------------------------------------------------------------------
915
- def _enable_refresh_button(self):
916
- self.refresh_button.setEnabled(True)
187
+ widget.plugin_api_version != 1
188
+ and action_name == InstallerActions.UNINSTALL
189
+ ):
190
+ _show_message(widget)
917
191
 
918
- def _quit(self):
919
- self.close()
920
- with contextlib.suppress(AttributeError):
921
- self._parent.close(quit_app=True, confirm_need=True)
922
192
 
923
- 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)
193
+ class QtPluginDialog(BaseQtPluginDialog):
928
194
 
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)
195
+ PACKAGE_METADATA_CLASS = npe2.PackageMetadata
196
+ PROJECT_INFO_VERSION_CLASS = ProjectInfoVersions
197
+ PLUGIN_LIST_CLASS = QPluginList
198
+ INSTALLER_QUEUE_CLASS = NapariInstallerQueue
199
+ BASE_PACKAGE_NAME = 'napari'
934
200
 
935
- self._close_shortcut = QShortcut(
936
- QKeySequence(Qt.CTRL | Qt.Key_W), self
937
- )
938
- self._close_shortcut.activated.connect(self.close)
201
+ def _setup_theme_update(self):
939
202
  get_settings().appearance.events.theme.connect(self._update_theme)
940
203
 
941
204
  def _update_theme(self, event):
942
205
  stylesheet = get_current_stylesheet([STYLES_PATH])
943
206
  self.setStyleSheet(stylesheet)
944
207
 
945
- def _on_installer_start(self):
946
- """Updates dialog buttons and status when installing a plugin."""
947
- self.cancel_all_btn.setVisible(True)
948
- self.working_indicator.show()
949
- self.process_success_indicator.hide()
950
- self.process_error_indicator.hide()
951
- self.refresh_button.setDisabled(True)
952
-
953
- def _on_process_finished(self, process_finished_data: ProcessFinishedData):
954
- action = process_finished_data['action']
955
- exit_code = process_finished_data['exit_code']
956
- pkg_names = [
957
- pkg.split('==')[0] for pkg in process_finished_data['pkgs']
958
- ]
959
- if action == InstallerActions.INSTALL:
960
- if exit_code == 0:
961
- for pkg_name in pkg_names:
962
- if pkg_name in self.available_set:
963
- self.available_set.remove(pkg_name)
964
-
965
- self.available_list.removeItem(pkg_name)
966
- self._add_installed(pkg_name)
967
- self._tag_outdated_plugins()
968
- else:
969
- for pkg_name in pkg_names:
970
- self.available_list.refreshItem(pkg_name)
971
- elif action == InstallerActions.UNINSTALL:
972
- if exit_code == 0:
973
- for pkg_name in pkg_names:
974
- if pkg_name in self.already_installed:
975
- self.already_installed.remove(pkg_name)
976
-
977
- self.installed_list.removeItem(pkg_name)
978
- self._add_to_available(pkg_name)
979
- else:
980
- for pkg_name in pkg_names:
981
- self.installed_list.refreshItem(pkg_name)
982
- elif action == InstallerActions.UPGRADE:
983
- pkg_info = [
984
- (pkg.split('==')[0], pkg.split('==')[1])
985
- for pkg in process_finished_data['pkgs']
986
- ]
987
- for pkg_name, pkg_version in pkg_info:
988
- self.installed_list.refreshItem(pkg_name, version=pkg_version)
989
- self._tag_outdated_plugins()
990
- elif action in [InstallerActions.CANCEL, InstallerActions.CANCEL_ALL]:
991
- for pkg_name in pkg_names:
992
- self.installed_list.refreshItem(pkg_name)
993
- self.available_list.refreshItem(pkg_name)
994
- self._tag_outdated_plugins()
995
-
996
- self.working_indicator.hide()
997
- if exit_code:
998
- self.process_error_indicator.show()
999
- else:
1000
- self.process_success_indicator.show()
1001
-
1002
- def _on_installer_all_finished(self, exit_codes):
1003
- self.working_indicator.hide()
1004
- self.cancel_all_btn.setVisible(False)
1005
- self.close_btn.setDisabled(False)
1006
- self.refresh_button.setDisabled(False)
1007
-
1008
- if not self.isVisible():
1009
- if sum(exit_codes) > 0:
1010
- show_warning(
1011
- trans._('Plugin Manager: process completed with errors\n')
1012
- )
1013
- else:
1014
- show_info(trans._('Plugin Manager: process completed\n'))
1015
-
1016
- def _add_to_available(self, pkg_name):
1017
- self._add_items_timer.stop()
1018
- self._plugin_queue.insert(0, self._plugin_data_map[pkg_name])
1019
- self._add_items_timer.start()
1020
- self._update_plugin_count()
1021
-
1022
- def _add_to_installed(self, distname, enabled, npe_version=1):
1023
- norm_name = normalized_name(distname or '')
1024
- if distname:
1025
- try:
1026
- meta = importlib.metadata.metadata(distname)
1027
-
1028
- except importlib.metadata.PackageNotFoundError:
1029
- return # a race condition has occurred and the package is uninstalled by another thread
1030
- if len(meta) == 0:
1031
- # will not add builtins.
1032
- return
1033
- self.already_installed.add(norm_name)
1034
- else:
1035
- meta = {}
1036
-
1037
- self.installed_list.addItem(
1038
- ProjectInfoVersions(
1039
- npe2.PackageMetadata(
1040
- metadata_version="1.0",
1041
- name=norm_name,
1042
- version=meta.get('version', ''),
1043
- summary=meta.get('summary', ''),
1044
- home_page=meta.get('Home-page', ''),
1045
- author=meta.get('author', ''),
1046
- license=meta.get('license', ''),
1047
- ),
1048
- norm_name,
1049
- [],
1050
- [],
1051
- ),
1052
- installed=True,
1053
- enabled=enabled,
1054
- npe_version=npe_version,
1055
- )
1056
-
1057
208
  def _add_installed(self, pkg_name=None):
1058
209
  pm2 = npe2.PluginManager.instance()
1059
210
  pm2.discover()
@@ -1065,7 +216,9 @@ class QtPluginDialog(QDialog):
1065
216
  # if it's an Npe1 adaptor, call it v1
1066
217
  npev = 'shim' if manifest.npe1_shim else 2
1067
218
  if distname == pkg_name or pkg_name is None:
1068
- self._add_to_installed(distname, enabled, npe_version=npev)
219
+ self._add_to_installed(
220
+ distname, enabled, distname, plugin_api_version=npev
221
+ )
1069
222
 
1070
223
  napari.plugins.plugin_manager.discover() # since they might not be loaded yet
1071
224
  for (
@@ -1085,6 +238,7 @@ class QtPluginDialog(QDialog):
1085
238
  self._add_to_installed(
1086
239
  distname,
1087
240
  not napari.plugins.plugin_manager.is_blocked(plugin_name),
241
+ normalized_name(distname or ''),
1088
242
  )
1089
243
  self._update_plugin_count()
1090
244
 
@@ -1094,7 +248,7 @@ class QtPluginDialog(QDialog):
1094
248
  if widget.name == pkg_name:
1095
249
  self.installed_list.scrollToItem(item)
1096
250
  self.installed_list.setCurrentItem(item)
1097
- if widget.npe_version != 1:
251
+ if widget.plugin_api_version != 1:
1098
252
  _show_message(widget)
1099
253
  break
1100
254
 
@@ -1108,436 +262,30 @@ class QtPluginDialog(QDialog):
1108
262
  self.worker.yielded.connect(self._handle_yield)
1109
263
  self.worker.started.connect(self.working_indicator.show)
1110
264
  self.worker.finished.connect(self.working_indicator.hide)
1111
- self.worker.finished.connect(self._add_items_timer.start)
265
+ self.worker.finished.connect(self.finished)
266
+ self.worker.finished.connect(self.search)
1112
267
  self.worker.start()
1113
268
 
1114
269
  pm2 = npe2.PluginManager.instance()
1115
270
  pm2.discover()
1116
271
 
1117
- def _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()
272
+ def _loading_gif(self):
1187
273
  load_gif = str(Path(napari.resources.__file__).parent / "loading.gif")
1188
274
  mov = QMovie(load_gif)
1189
275
  mov.setScaledSize(QSize(18, 18))
1190
- 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()
276
+ return mov
1519
277
 
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()
278
+ def _on_bundle(self):
279
+ return running_as_constructor_app()
1528
280
 
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
281
+ def _show_info(self, info):
282
+ show_info(info)
1535
283
 
1536
- for idx in range(self.installed_list.count()):
1537
- item = self.installed_list.item(idx)
1538
- item.widget.prefix = prefix
284
+ def _show_warning(self, warning):
285
+ show_warning(warning)
1539
286
 
1540
- # endregion - Public methods
287
+ def _trans(self, text, **kwargs):
288
+ return trans._(text, **kwargs)
1541
289
 
1542
290
 
1543
291
  if __name__ == "__main__":