-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpackage_list.py
792 lines (683 loc) · 34.6 KB
/
package_list.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2022-2023 FreeCAD Project Association *
# * *
# * This file is part of FreeCAD. *
# * *
# * FreeCAD is free software: you can redistribute it and/or modify it *
# * under the terms of the GNU Lesser General Public License as *
# * published by the Free Software Foundation, either version 2.1 of the *
# * License, or (at your option) any later version. *
# * *
# * FreeCAD is distributed in the hope that it will be useful, but *
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
# * Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Lesser General Public *
# * License along with FreeCAD. If not, see *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
"""Defines the PackageList QWidget for displaying a list of Addons."""
import datetime
import threading
import FreeCAD
from PySide import QtCore, QtGui, QtWidgets
from Addon import Addon
from compact_view import Ui_CompactView
from expanded_view import Ui_ExpandedView
import addonmanager_utilities as utils
from addonmanager_metadata import get_first_supported_freecad_version, Version
from Widgets.addonmanager_widget_view_control_bar import WidgetViewControlBar, SortOptions
from Widgets.addonmanager_widget_view_selector import AddonManagerDisplayStyle
from Widgets.addonmanager_widget_filter_selector import StatusFilter, Filter
from Widgets.addonmanager_widget_progress_bar import Progress, WidgetProgressBar
from addonmanager_licenses import get_license_manager
translate = FreeCAD.Qt.translate
# pylint: disable=too-few-public-methods
class PackageList(QtWidgets.QWidget):
"""A widget that shows a list of packages and various widgets to control the
display of the list, including a progress bar that can display and interrupt the load
process."""
itemSelected = QtCore.Signal(Addon)
stop_loading = QtCore.Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_PackageList()
self.ui.setupUi(self)
self.item_filter = PackageListFilter()
self.ui.listPackages.setModel(self.item_filter)
self.item_delegate = PackageListItemDelegate(self.ui.listPackages)
self.ui.listPackages.setItemDelegate(self.item_delegate)
self.ui.listPackages.clicked.connect(self.on_listPackages_clicked)
self.ui.view_bar.filter_changed.connect(self.update_status_filter)
self.ui.view_bar.search_changed.connect(self.item_filter.setFilterRegularExpression)
self.ui.view_bar.sort_changed.connect(self.item_filter.setSortRole)
self.ui.view_bar.sort_changed.connect(self.item_delegate.set_sort)
self.ui.view_bar.sort_order_changed.connect(lambda order: self.item_filter.sort(0, order))
self.ui.progress_bar.stop_clicked.connect(self.stop_loading)
# Set up the view the same as the last time:
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
package_type = pref.GetInt("PackageTypeSelection", 0)
status = pref.GetInt("StatusSelection", 0)
search_string = pref.GetString("SearchString", "")
self.ui.view_bar.filter_selector.set_contents_filter(package_type)
self.ui.view_bar.filter_selector.set_status_filter(status)
if search_string:
self.ui.view_bar.search.filter_line_edit.setText(search_string)
self.item_filter.setPackageFilter(package_type)
self.item_filter.setStatusFilter(status)
# Pre-init of other members:
self.item_model = None
def setModel(self, model):
"""This is a model-view-controller widget: set its model."""
self.item_model = model
self.item_filter.setSourceModel(self.item_model)
self.item_filter.setSortRole(SortOptions.Alphabetical)
self.item_filter.sort(0, QtCore.Qt.AscendingOrder)
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
style = pref.GetInt("ViewStyle", AddonManagerDisplayStyle.EXPANDED)
self.set_view_style(style)
self.ui.view_bar.view_selector.set_current_view(style)
self.item_filter.setHidePy2(pref.GetBool("HidePy2", False))
self.item_filter.setHideObsolete(pref.GetBool("HideObsolete", False))
self.item_filter.setHideNonOSIApproved(pref.GetBool("HideNonOSIApproved", False))
self.item_filter.setHideNonFSFLibre(pref.GetBool("HideNonFSFFreeLibre", False))
self.item_filter.setHideNewerFreeCADRequired(
pref.GetBool("HideNewerFreeCADRequired", False)
)
self.item_filter.setHideUnlicensed(pref.GetBool("HideUnlicensed", False))
def select_addon(self, addon_name: str):
for index, addon in enumerate(self.item_model.repos):
if addon.name == addon_name:
row_index = self.item_model.createIndex(index, 0)
if self.item_filter.filterAcceptsRow(index):
self.ui.listPackages.setCurrentIndex(row_index)
else:
FreeCAD.Console.PrintLog(
f"Addon {addon_name} is not visible given current "
"filter: not selecting it."
)
return
FreeCAD.Console.PrintLog(f"Could not find addon '{addon_name}' to select it")
def on_listPackages_clicked(self, index: QtCore.QModelIndex):
"""Determine what addon was selected and emit the itemSelected signal with it as
an argument."""
source_selection = self.item_filter.mapToSource(index)
selected_repo = self.item_model.repos[source_selection.row()]
self.itemSelected.emit(selected_repo)
def update_status_filter(self, new_filter: Filter) -> None:
"""hide/show rows corresponding to the specified filter"""
self.item_filter.setStatusFilter(new_filter.status_filter)
self.item_filter.setPackageFilter(new_filter.content_filter)
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetInt("StatusSelection", new_filter.status_filter)
pref.SetInt("PackageTypeSelection", new_filter.content_filter)
self.item_filter.invalidateFilter()
def set_view_style(self, style: AddonManagerDisplayStyle) -> None:
"""Set the style (compact or expanded) of the list"""
if self.item_model:
self.item_model.layoutAboutToBeChanged.emit()
self.item_delegate.set_view(style)
if style == AddonManagerDisplayStyle.COMPACT or style == AddonManagerDisplayStyle.COMPOSITE:
self.ui.listPackages.setSpacing(2)
self.ui.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerItem)
self.ui.listPackages.verticalScrollBar().setSingleStep(-1)
else:
self.ui.listPackages.setSpacing(5)
self.ui.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.ui.listPackages.verticalScrollBar().setSingleStep(24)
if self.item_model:
self.item_model.layoutChanged.emit()
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetInt("ViewStyle", style)
def set_loading(self, is_loading: bool) -> None:
"""Set the loading status of this package list: when a package list is loading, it shows
a progress bar. When it is no longer loading, the bar is hidden and the search bar gets
the focus."""
if is_loading:
self.ui.progress_bar.show()
else:
self.ui.progress_bar.hide()
self.ui.view_bar.search.setFocus()
def update_loading_progress(self, progress: Progress) -> None:
self.ui.progress_bar.set_progress(progress)
class PackageListItemModel(QtCore.QAbstractListModel):
"""The model for use with the PackageList class."""
repos = []
write_lock = threading.Lock()
DataAccessRole = QtCore.Qt.UserRole
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
"""The number of rows"""
if parent.isValid():
return 0
return len(self.repos)
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
"""Only one column, always returns 1."""
if parent.isValid():
return 0
return 1
def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole):
"""Get the data for a given index and role."""
if not index.isValid():
return None
row = index.row()
if role == QtCore.Qt.ToolTipRole:
tooltip = ""
if self.repos[row].repo_type == Addon.Kind.PACKAGE:
tooltip = translate("AddonsInstaller", "Click for details about package {}").format(
self.repos[row].display_name
)
elif self.repos[row].repo_type == Addon.Kind.WORKBENCH:
tooltip = translate(
"AddonsInstaller", "Click for details about workbench {}"
).format(self.repos[row].display_name)
elif self.repos[row].repo_type == Addon.Kind.MACRO:
tooltip = translate("AddonsInstaller", "Click for details about macro {}").format(
self.repos[row].display_name
)
return tooltip
if role == PackageListItemModel.DataAccessRole:
return self.repos[row]
# Sorting
if role == SortOptions.Alphabetical:
return self.repos[row].display_name
if role == SortOptions.LastUpdated:
update_date = self.repos[row].update_date
if update_date and hasattr(update_date, "timestamp"):
return update_date.timestamp()
return 0
if role == SortOptions.DateAdded:
if self.repos[row].stats and self.repos[row].stats.date_created:
return self.repos[row].stats.date_created.timestamp()
return 0
if role == SortOptions.Stars:
if self.repos[row].stats and self.repos[row].stats.stars:
return self.repos[row].stats.stars
return 0
if role == SortOptions.Score:
return self.repos[row].score
def headerData(self, _unused1, _unused2, _role=QtCore.Qt.DisplayRole):
"""No header in this implementation: always returns None."""
return None
def append_item(self, repo: Addon) -> None:
"""Adds this addon to the end of the model. Thread safe."""
if repo in self.repos:
# Cowardly refuse to insert the same repo a second time
return
with self.write_lock:
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self.repos.append(repo)
self.endInsertRows()
def clear(self) -> None:
"""Clear the model, removing all rows. Thread safe."""
if self.rowCount() > 0:
with self.write_lock:
self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount() - 1)
self.repos = []
self.endRemoveRows()
def reload_item(self, repo: Addon) -> None:
"""Sets the addon data for the given addon (based on its name)"""
for index, item in enumerate(self.repos):
if item.name == repo.name:
with self.write_lock:
self.repos[index] = repo
return
class CompactView(QtWidgets.QWidget):
"""A single-line view of the package information"""
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_CompactView()
self.ui.setupUi(self)
class ExpandedView(QtWidgets.QWidget):
"""A multi-line view of the package information"""
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_ExpandedView()
self.ui.setupUi(self)
class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
"""Render the repo data as a formatted region"""
def __init__(self, parent=None):
super().__init__(parent)
self.displayStyle = AddonManagerDisplayStyle.EXPANDED
self.sort_order = SortOptions.Alphabetical
self.expanded = ExpandedView()
self.compact = CompactView()
self.widget = self.expanded
def set_view(self, style: AddonManagerDisplayStyle) -> None:
"""Set the view of to style"""
if not self.displayStyle == style:
self.displayStyle = style
def set_sort(self, sort: SortOptions) -> None:
"""When sorting by various things, we display the thing that's being sorted on."""
if not self.sort_order == sort:
self.sort_order = sort
def sizeHint(self, _option, index):
"""Attempt to figure out the correct height for the widget based on its
current contents."""
self.update_content(index)
return self.widget.sizeHint()
def update_content(self, index):
"""Creates the display of the content for a given index."""
repo = index.data(PackageListItemModel.DataAccessRole)
if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
self.widget = self.expanded
self._setup_expanded_view(repo)
elif self.displayStyle == AddonManagerDisplayStyle.COMPACT:
self.widget = self.compact
self._setup_compact_view(repo)
elif self.displayStyle == AddonManagerDisplayStyle.COMPOSITE:
self.widget = self.compact # For now reuse the compact list
self._setup_composite_view(repo)
self.widget.adjustSize()
def _setup_expanded_view(self, addon: Addon) -> None:
self.widget.ui.labelPackageName.setText(f"<h1>{addon.display_name}</h1>")
self.widget.ui.labelIcon.setPixmap(addon.icon.pixmap(QtCore.QSize(48, 48)))
self.widget.ui.labelStatus.setText(self.get_expanded_update_string(addon))
self.widget.ui.labelIcon.setText("")
self.widget.ui.labelTags.setText("")
if addon.metadata:
self.widget.ui.labelDescription.setText(addon.metadata.description)
self.widget.ui.labelVersion.setText(f"<i>v{addon.metadata.version}</i>")
self._set_package_maintainer_label(addon)
elif addon.macro:
self.widget.ui.labelDescription.setText(addon.macro.comment)
self._set_macro_version_label(addon)
self._set_macro_maintainer_label(addon)
else:
self.widget.ui.labelDescription.setText("")
self.widget.ui.labelMaintainer.setText("")
self.widget.ui.labelVersion.setText("")
if addon.tags:
self.widget.ui.labelTags.setText(
translate("AddonsInstaller", "Tags") + ": " + ", ".join(addon.tags)
)
if self.sort_order == SortOptions.Alphabetical:
self.widget.ui.labelSort.setText("")
else:
self.widget.ui.labelSort.setText(self._get_sort_label_text(addon))
def _setup_compact_view(self, addon: Addon) -> None:
self.widget.ui.labelPackageName.setText(f"<b>{addon.display_name}</b>")
self.widget.ui.labelIcon.setPixmap(addon.icon.pixmap(QtCore.QSize(16, 16)))
self.widget.ui.labelStatus.setText(self.get_compact_update_string(addon))
self.widget.ui.labelIcon.setText("")
if addon.metadata:
self.widget.ui.labelVersion.setText(f"<i>v{addon.metadata.version}</i>")
elif addon.macro:
self._set_macro_version_label(addon)
else:
self.widget.ui.labelVersion.setText("")
if self.sort_order == SortOptions.Alphabetical:
description = self._get_compact_description(addon)
self.widget.ui.labelDescription.setText(description)
else:
self.widget.ui.labelDescription.setText(self._get_sort_label_text(addon))
def _setup_composite_view(self, addon: Addon) -> None:
self.widget.ui.labelPackageName.setText(f"<b>{addon.display_name}</b>")
self.widget.ui.labelIcon.setPixmap(addon.icon.pixmap(QtCore.QSize(16, 16)))
self.widget.ui.labelStatus.setText(self.get_compact_update_string(addon))
self.widget.ui.labelIcon.setText("")
if addon.metadata:
self.widget.ui.labelVersion.setText(f"<i>v{addon.metadata.version}</i>")
elif addon.macro:
self._set_macro_version_label(addon)
else:
self.widget.ui.labelVersion.setText("")
if self.sort_order != SortOptions.Alphabetical:
self.widget.ui.labelDescription.setText(self._get_sort_label_text(addon))
else:
self.widget.ui.labelDescription.setText("")
def _set_package_maintainer_label(self, addon: Addon):
maintainers = addon.metadata.maintainer
maintainers_string = ""
if len(maintainers) == 1:
maintainers_string = (
translate("AddonsInstaller", "Maintainer")
+ f": {maintainers[0].name} <{maintainers[0].email}>"
)
elif len(maintainers) > 1:
n = len(maintainers)
maintainers_string = translate("AddonsInstaller", "Maintainers:", "", n)
for maintainer in maintainers:
maintainers_string += f"\n{maintainer.name} <{maintainer.email}>"
self.widget.ui.labelMaintainer.setText(maintainers_string)
def _set_macro_maintainer_label(self, repo: Addon):
if repo.macro.author:
caption = translate("AddonsInstaller", "Author")
self.widget.ui.labelMaintainer.setText(caption + ": " + repo.macro.author)
else:
self.widget.ui.labelMaintainer.setText("")
def _set_macro_version_label(self, addon: Addon):
version_string = ""
if addon.macro.version:
version_string = addon.macro.version + " "
if addon.macro.on_wiki:
version_string += "(wiki)"
elif addon.macro.on_git:
version_string += "(git)"
else:
version_string += "(unknown source)"
self.widget.ui.labelVersion.setText("<i>" + version_string + "</i>")
def _get_sort_label_text(self, addon: Addon) -> str:
if self.sort_order == SortOptions.Alphabetical:
return ""
elif self.sort_order == SortOptions.Stars:
if addon.stats and addon.stats.stars and addon.stats.stars > 0:
return translate("AddonsInstaller", "{} ★ on GitHub").format(addon.stats.stars)
return translate("AddonsInstaller", "No ★, or not on GitHub")
elif self.sort_order == SortOptions.DateAdded:
if addon.stats and addon.stats.date_created:
epoch_seconds = addon.stats.date_created.timestamp()
qdt = QtCore.QDateTime.fromSecsSinceEpoch(int(epoch_seconds)).date()
time_string = QtCore.QLocale().toString(qdt, QtCore.QLocale.ShortFormat)
return translate("AddonsInstaller", "Created ") + time_string
return ""
elif self.sort_order == SortOptions.LastUpdated:
update_date = addon.update_date
if update_date:
epoch_seconds = update_date.timestamp()
qdt = QtCore.QDateTime.fromSecsSinceEpoch(int(epoch_seconds)).date()
time_string = QtCore.QLocale().toString(qdt, QtCore.QLocale.ShortFormat)
return translate("AddonsInstaller", "Updated ") + time_string
return ""
elif self.sort_order == SortOptions.Score:
return translate("AddonsInstaller", "Score: ") + str(addon.score)
return ""
def _get_compact_description(self, addon: Addon) -> str:
description = ""
if addon.metadata:
description = addon.metadata.description
elif addon.macro and addon.macro.comment:
description = addon.macro.comment
trimmed_text, _, _ = description.partition(".")
return trimmed_text.replace("\n", " ")
@staticmethod
def get_compact_update_string(repo: Addon) -> str:
"""Get a single-line string listing details about the installed version and
date"""
result = ""
if repo.status() == Addon.Status.UNCHECKED:
result = translate("AddonsInstaller", "Installed")
elif repo.status() == Addon.Status.NO_UPDATE_AVAILABLE:
result = translate("AddonsInstaller", "Up-to-date")
elif repo.status() == Addon.Status.UPDATE_AVAILABLE:
result = translate("AddonsInstaller", "Update available")
elif repo.status() == Addon.Status.PENDING_RESTART:
result = translate("AddonsInstaller", "Pending restart")
if repo.is_disabled():
style = "style='color:" + utils.warning_color_string() + "; font-weight:bold;'"
result += f"<span {style}> [" + translate("AddonsInstaller", "DISABLED") + "]</span>"
return result
@staticmethod
def get_expanded_update_string(repo: Addon) -> str:
"""Get a multi-line string listing details about the installed version and
date"""
result = ""
installed_version_string = ""
if repo.status() != Addon.Status.NOT_INSTALLED:
if repo.installed_version or repo.installed_metadata:
installed_version_string = (
"<br/>" + translate("AddonsInstaller", "Installed version") + ": "
)
if repo.installed_metadata:
installed_version_string += str(repo.installed_metadata.version)
elif repo.installed_version:
installed_version_string += str(repo.installed_version)
else:
installed_version_string = "<br/>" + translate("AddonsInstaller", "Unknown version")
installed_date_string = ""
if repo.updated_timestamp:
installed_date_string = "<br/>" + translate("AddonsInstaller", "Installed on") + ": "
installed_date_string += QtCore.QLocale().toString(
QtCore.QDateTime.fromSecsSinceEpoch(int(round(repo.updated_timestamp, 0))),
QtCore.QLocale.ShortFormat,
)
available_version_string = ""
if repo.metadata:
available_version_string = (
"<br/>" + translate("AddonsInstaller", "Available version") + ": "
)
available_version_string += str(repo.metadata.version)
if repo.status() == Addon.Status.UNCHECKED:
result = translate("AddonsInstaller", "Installed")
result += installed_version_string
result += installed_date_string
elif repo.status() == Addon.Status.NO_UPDATE_AVAILABLE:
result = translate("AddonsInstaller", "Up-to-date")
result += installed_version_string
result += installed_date_string
elif repo.status() == Addon.Status.UPDATE_AVAILABLE:
result = translate("AddonsInstaller", "Update available")
result += installed_version_string
result += installed_date_string
result += available_version_string
elif repo.status() == Addon.Status.PENDING_RESTART:
result = translate("AddonsInstaller", "Pending restart")
if repo.is_disabled():
style = "style='color:" + utils.warning_color_string() + "; font-weight:bold;'"
result += (
f"<br/><span {style}>[" + translate("AddonsInstaller", "DISABLED") + "]</span>"
)
return result
def paint(
self,
painter: QtGui.QPainter,
option: QtWidgets.QStyleOptionViewItem,
_: QtCore.QModelIndex,
):
"""Main paint function: renders this widget into a given rectangle,
successively drawing all of its children."""
painter.save()
self.widget.resize(option.rect.size())
painter.translate(option.rect.topLeft())
self.widget.render(
painter, QtCore.QPoint(), QtGui.QRegion(), QtWidgets.QWidget.DrawChildren
)
painter.restore()
class PackageListFilter(QtCore.QSortFilterProxyModel):
"""Handle filtering the item list on various criteria"""
def __init__(self):
super().__init__()
self.package_type = 0 # Default to showing everything
self.status = 0 # Default to showing any
self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.hide_obsolete = False
self.hide_py2 = False
self.hide_non_OSI_approved = False
self.hide_non_FSF_libre = False
self.hide_unlicensed = False
self.hide_newer_freecad_required = False
def setPackageFilter(
self, package_type: int
) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs, 4=Bundles, 5=Other
"""Set the package filter to package_type and refreshes."""
self.package_type = package_type
self.invalidateFilter()
def setStatusFilter(
self, status: int
) -> None: # 0=Any, 1=Installed, 2=Not installed, 3=Update available
"""Sets the status filter to status and refreshes."""
self.status = status
self.invalidateFilter()
def setHidePy2(self, hide_py2: bool) -> None:
"""Sets whether to hide Python 2-only Addons"""
self.hide_py2 = hide_py2
self.invalidateFilter()
def setHideObsolete(self, hide_obsolete: bool) -> None:
"""Sets whether to hide Addons marked obsolete"""
self.hide_obsolete = hide_obsolete
self.invalidateFilter()
def setHideNonOSIApproved(self, hide: bool) -> None:
"""Sets whether to hide Addons with non-OSI-approved licenses"""
self.hide_non_OSI_approved = hide
self.invalidateFilter()
def setHideNonFSFLibre(self, hide: bool) -> None:
"""Sets whether to hide Addons with non-FSF-Libre licenses"""
self.hide_non_FSF_libre = hide
self.invalidateFilter()
def setHideUnlicensed(self, hide: bool) -> None:
"""Sets whether to hide addons without a specified license"""
self.hide_unlicensed = hide
self.invalidateFilter()
def setHideNewerFreeCADRequired(self, hide_nfr: bool) -> None:
"""Sets whether to hide packages that have indicated they need a newer version
of FreeCAD than the one currently running."""
self.hide_newer_freecad_required = hide_nfr
self.invalidateFilter()
# def lessThan(self, left_in, right_in) -> bool:
# """Enable sorting of display name (not case-sensitive)."""
#
# left = self.sourceModel().data(left_in, self.sortRole)
# right = self.sourceModel().data(right_in, self.sortRole)
#
# return left.display_name.lower() < right.display_name.lower()
def filterAcceptsRow(self, row, _parent=QtCore.QModelIndex()):
"""Do the actual filtering (called automatically by Qt when drawing the list)"""
index = self.sourceModel().createIndex(row, 0)
data = self.sourceModel().data(index, PackageListItemModel.DataAccessRole)
if self.package_type == 1:
if not data.contains_workbench():
return False
elif self.package_type == 2:
if not data.contains_macro():
return False
elif self.package_type == 3:
if not data.contains_preference_pack():
return False
elif self.package_type == 4:
if not data.contains_bundle():
return False
elif self.package_type == 5:
if not data.contains_other():
return False
if self.status == StatusFilter.INSTALLED:
if data.status() == Addon.Status.NOT_INSTALLED:
return False
elif self.status == StatusFilter.NOT_INSTALLED:
if data.status() != Addon.Status.NOT_INSTALLED:
return False
elif self.status == StatusFilter.UPDATE_AVAILABLE:
if data.status() != Addon.Status.UPDATE_AVAILABLE:
return False
license_manager = get_license_manager()
if data.status() == Addon.Status.NOT_INSTALLED:
# If it's not installed, check to see if it's Py2 only
if self.hide_py2 and data.python2:
return False
# If it's not installed, check to see if it's marked obsolete
if self.hide_obsolete and data.obsolete:
return False
if self.hide_unlicensed:
if not data.license or data.license in ["UNLICENSED", "UNLICENCED"]:
FreeCAD.Console.PrintLog(f"Hiding {data.name} because it has no license set\n")
return False
# If it is not an OSI-approved license, check to see if we are hiding those
if self.hide_non_OSI_approved or self.hide_non_FSF_libre:
if not data.license:
return False
licenses_to_check = []
if type(data.license) is str:
licenses_to_check.append(data.license)
elif type(data.license) is list:
for license_id in data.license:
if type(license_id) is str:
licenses_to_check.append(license_id)
else:
licenses_to_check.append(license_id.name)
else:
licenses_to_check.append(data.license.name)
fsf_libre = False
osi_approved = False
for license_id in licenses_to_check:
if not osi_approved and license_manager.is_osi_approved(license_id):
osi_approved = True
if not fsf_libre and license_manager.is_fsf_libre(license_id):
fsf_libre = True
if self.hide_non_OSI_approved and not osi_approved:
# FreeCAD.Console.PrintLog(
# f"Hiding addon {data.name} because its license, {licenses_to_check}, "
# f"is "
# f"not OSI approved\n"
# )
return False
if self.hide_non_FSF_libre and not fsf_libre:
# FreeCAD.Console.PrintLog(
# f"Hiding addon {data.name} because its license, {licenses_to_check}, is "
# f"not FSF Libre\n"
# )
return False
# If it's not installed, check to see if it's for a newer version of FreeCAD
if (
data.status() == Addon.Status.NOT_INSTALLED
and self.hide_newer_freecad_required
and data.metadata
):
# Only hide if ALL content items require a newer version, otherwise
# it's possible that this package actually provides versions of itself
# for newer and older versions
first_supported_version = get_first_supported_freecad_version(data.metadata)
if first_supported_version is not None:
current_fc_version = Version(from_list=FreeCAD.Version())
if first_supported_version > current_fc_version:
return False
name = data.display_name
desc = data.description
if hasattr(self, "filterRegularExpression"): # Added in Qt 5.12
re = self.filterRegularExpression()
if re.isValid():
re.setPatternOptions(QtCore.QRegularExpression.CaseInsensitiveOption)
if re.match(name).hasMatch():
return True
if re.match(desc).hasMatch():
return True
if data.macro and data.macro.comment and re.match(data.macro.comment).hasMatch():
return True
for tag in data.tags:
if re.match(tag).hasMatch():
return True
return False
# Only get here for Qt < 5.12
re = self.filterRegExp()
if re.isValid():
re.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
if re.indexIn(name) != -1:
return True
if re.indexIn(desc) != -1:
return True
if data.macro and data.macro.comment and re.indexIn(data.macro.comment) != -1:
return True
for tag in data.tags:
if re.indexIn(tag) != -1:
return True
return False
# pylint: disable=attribute-defined-outside-init, missing-function-docstring
class Ui_PackageList:
"""The contents of the PackageList widget"""
def setupUi(self, form):
if not form.objectName():
form.setObjectName("PackageList")
self.verticalLayout = QtWidgets.QVBoxLayout(form)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
self.view_bar = WidgetViewControlBar(form)
self.view_bar.setObjectName("ViewControlBar")
self.horizontalLayout_6.addWidget(self.view_bar)
self.verticalLayout.addLayout(self.horizontalLayout_6)
self.listPackages = QtWidgets.QListView(form)
self.listPackages.setObjectName("listPackages")
self.listPackages.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.listPackages.setProperty("showDropIndicator", False)
self.listPackages.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.listPackages.setResizeMode(QtWidgets.QListView.Adjust)
self.listPackages.setUniformItemSizes(False)
self.listPackages.setAlternatingRowColors(True)
self.listPackages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.listPackages.verticalScrollBar().setSingleStep(24)
self.verticalLayout.addWidget(self.listPackages)
self.progress_bar = WidgetProgressBar()
self.verticalLayout.addWidget(self.progress_bar)
QtCore.QMetaObject.connectSlotsByName(form)