771 lines
34 KiB
Python
771 lines
34 KiB
Python
# 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 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"""
|
|
|
|
itemSelected = QtCore.Signal(Addon)
|
|
|
|
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))
|
|
|
|
# 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)
|
|
|
|
|
|
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 re-use 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
|
|
"""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
|
|
|
|
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.progressBar = WidgetProgressBar()
|
|
self.verticalLayout.addWidget(self.progressBar)
|
|
|
|
QtCore.QMetaObject.connectSlotsByName(form)
|