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