Addon Manager: Add sorting (#12561)

This commit is contained in:
Chris Hennes
2024-02-23 22:33:20 -06:00
committed by GitHub
parent bc935c028f
commit 7d824fc774
13 changed files with 567 additions and 132 deletions

View File

@@ -22,8 +22,7 @@
# ***************************************************************************
""" Defines the PackageList QWidget for displaying a list of Addons. """
from enum import IntEnum
import datetime
import threading
import FreeCAD
@@ -37,11 +36,11 @@ 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
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, ContentFilter
from Widgets.addonmanager_widget_filter_selector import StatusFilter, Filter
from Widgets.addonmanager_widget_progress_bar import WidgetProgressBar
from addonmanager_licenses import get_license_manager, SPDXLicenseManager
from addonmanager_licenses import get_license_manager
translate = FreeCAD.Qt.translate
@@ -69,6 +68,9 @@ class PackageList(QtWidgets.QWidget):
self.ui.view_bar.view_changed.connect(self.set_view_style)
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")
@@ -86,7 +88,8 @@ class PackageList(QtWidgets.QWidget):
"""This is a model-view-controller widget: set its model."""
self.item_model = model
self.item_filter.setSourceModel(self.item_model)
self.item_filter.sort(0)
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)
@@ -141,8 +144,6 @@ class PackageListItemModel(QtCore.QAbstractListModel):
write_lock = threading.Lock()
DataAccessRole = QtCore.Qt.UserRole
StatusUpdateRole = QtCore.Qt.UserRole + 1
IconUpdateRole = QtCore.Qt.UserRole + 2
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
"""The number of rows"""
@@ -179,30 +180,29 @@ class PackageListItemModel(QtCore.QAbstractListModel):
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.Rank:
return len(self.repos[row].display_name)
def headerData(self, _unused1, _unused2, _role=QtCore.Qt.DisplayRole):
"""No header in this implementation: always returns None."""
return None
def setData(self, index: QtCore.QModelIndex, value, role=QtCore.Qt.EditRole) -> None:
"""Set the data for this row. The column of the index is ignored."""
row = index.row()
with self.write_lock:
if role == PackageListItemModel.StatusUpdateRole:
self.repos[row].set_status(value)
self.dataChanged.emit(
self.index(row, 2),
self.index(row, 2),
[PackageListItemModel.StatusUpdateRole],
)
elif role == PackageListItemModel.IconUpdateRole:
self.repos[row].icon = value
self.dataChanged.emit(
self.index(row, 0),
self.index(row, 0),
[PackageListItemModel.IconUpdateRole],
)
def append_item(self, repo: Addon) -> None:
"""Adds this addon to the end of the model. Thread safe."""
if repo in self.repos:
@@ -221,20 +221,6 @@ class PackageListItemModel(QtCore.QAbstractListModel):
self.repos = []
self.endRemoveRows()
def update_item_status(self, name: str, status: Addon.Status) -> None:
"""Set the status of addon with name to status."""
for row, item in enumerate(self.repos):
if item.name == name:
self.setData(self.index(row, 0), status, PackageListItemModel.StatusUpdateRole)
return
def update_item_icon(self, name: str, icon: QtGui.QIcon) -> None:
"""Set the icon for Addon with name to icon"""
for row, item in enumerate(self.repos):
if item.name == name:
self.setData(self.index(row, 0), icon, PackageListItemModel.IconUpdateRole)
return
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):
@@ -268,6 +254,7 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
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
@@ -277,6 +264,11 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
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."""
@@ -288,40 +280,58 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
repo = index.data(PackageListItemModel.DataAccessRole)
if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
self.widget = self.expanded
self.widget.ui.labelPackageName.setText(f"<h1>{repo.display_name}</h1>")
self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QtCore.QSize(48, 48)))
else:
self._setup_expanded_view(repo)
elif self.displayStyle == AddonManagerDisplayStyle.COMPACT:
self.widget = self.compact
self.widget.ui.labelPackageName.setText(f"<b>{repo.display_name}</b>")
self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QtCore.QSize(16, 16)))
self.widget.ui.labelIcon.setText("")
if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
self.widget.ui.labelTags.setText("")
if repo.metadata:
self.widget.ui.labelDescription.setText(repo.metadata.description)
self.widget.ui.labelVersion.setText(f"<i>v{repo.metadata.version}</i>")
if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
self._setup_expanded_package(repo)
elif repo.macro and repo.macro.parsed:
self._setup_macro(repo)
else:
self.widget.ui.labelDescription.setText("")
self.widget.ui.labelVersion.setText("")
if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
self.widget.ui.labelMaintainer.setText("")
# Update status
if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
self.widget.ui.labelStatus.setText(self.get_expanded_update_string(repo))
else:
self.widget.ui.labelStatus.setText(self.get_compact_update_string(repo))
self._setup_compact_view(repo)
self.widget.adjustSize()
def _setup_expanded_package(self, repo: Addon):
"""Set up the display for a package in expanded view"""
maintainers = repo.metadata.maintainer
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 _set_package_maintainer_label(self, addon: Addon):
maintainers = addon.metadata.maintainer
maintainers_string = ""
if len(maintainers) == 1:
maintainers_string = (
@@ -334,38 +344,64 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
for maintainer in maintainers:
maintainers_string += f"\n{maintainer.name} <{maintainer.email}>"
self.widget.ui.labelMaintainer.setText(maintainers_string)
if repo.tags:
self.widget.ui.labelTags.setText(
translate("AddonsInstaller", "Tags") + ": " + ", ".join(repo.tags)
)
def _setup_macro(self, repo: Addon):
"""Set up the display for a macro"""
self.widget.ui.labelDescription.setText(repo.macro.comment)
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 repo.macro.version:
version_string = repo.macro.version + " "
if repo.macro.on_wiki:
if addon.macro.version:
version_string = addon.macro.version + " "
if addon.macro.on_wiki:
version_string += "(wiki)"
elif repo.macro.on_git:
elif addon.macro.on_git:
version_string += "(git)"
else:
version_string += "(unknown source)"
if repo.macro.date:
version_string = (
version_string
+ ", "
+ translate("AddonsInstaller", "updated")
+ " "
+ repo.macro.date
)
self.widget.ui.labelVersion.setText("<i>" + version_string + "</i>")
if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
if repo.macro.author:
caption = translate("AddonsInstaller", "Author")
self.widget.ui.labelMaintainer.setText(caption + ": " + repo.macro.author)
else:
self.widget.ui.labelMaintainer.setText("")
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.Rank:
return translate("AddonsInstaller", "Rank: ") + str(len(addon.display_name))
return ""
def _set_sort_string_expanded(self, addon: Addon, label: QtWidgets.QLabel) -> None:
pass
def _get_compact_description(self, addon: Addon) -> str:
if addon.metadata:
trimmed_text = addon.metadata.description
# TODO: Un-hardcode the 25 character limiter
return trimmed_text.replace("\r\n", " ")[:25] + "..."
if addon.macro and addon.macro.comment:
trimmed_text = addon.macro.comment
return trimmed_text.replace("\r\n", " ")[:25] + "..."
return ""
@staticmethod
def get_compact_update_string(repo: Addon) -> str:
@@ -524,13 +560,13 @@ class PackageListFilter(QtCore.QSortFilterProxyModel):
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, PackageListItemModel.DataAccessRole)
right = self.sourceModel().data(right_in, PackageListItemModel.DataAccessRole)
return left.display_name.lower() < right.display_name.lower()
# 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)"""
@@ -597,17 +633,17 @@ class PackageListFilter(QtCore.QSortFilterProxyModel):
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"
)
# 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"
)
# 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