Addon Manager: Add sorting (#12561)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user