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