Addon Manager: Add sorting (#12561)
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
from typing import Dict, Set, List, Optional
|
||||
from threading import Lock
|
||||
@@ -176,14 +177,7 @@ class Addon:
|
||||
self.status_lock = Lock()
|
||||
self.update_status = status
|
||||
|
||||
# The url should never end in ".git", so strip it if it's there
|
||||
parsed_url = urlparse(self.url)
|
||||
if parsed_url.path.endswith(".git"):
|
||||
self.url = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path[:-4]
|
||||
if parsed_url.query:
|
||||
self.url += "?" + parsed_url.query
|
||||
if parsed_url.fragment:
|
||||
self.url += "#" + parsed_url.fragment
|
||||
self._clean_url()
|
||||
|
||||
if utils.recognized_git_location(self):
|
||||
self.metadata_url = construct_git_url(self, "package.xml")
|
||||
@@ -210,6 +204,17 @@ class Addon:
|
||||
|
||||
self._icon_file = None
|
||||
self._cached_license: str = ""
|
||||
self._cached_update_date = None
|
||||
|
||||
def _clean_url(self):
|
||||
# The url should never end in ".git", so strip it if it's there
|
||||
parsed_url = urlparse(self.url)
|
||||
if parsed_url.path.endswith(".git"):
|
||||
self.url = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path[:-4]
|
||||
if parsed_url.query:
|
||||
self.url += "?" + parsed_url.query
|
||||
if parsed_url.fragment:
|
||||
self.url += "#" + parsed_url.fragment
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = f"FreeCAD {self.repo_type}\n"
|
||||
@@ -235,6 +240,59 @@ class Addon:
|
||||
self._cached_license = "CC-BY-3.0"
|
||||
return self._cached_license
|
||||
|
||||
@property
|
||||
def update_date(self):
|
||||
if self._cached_update_date is None:
|
||||
self._cached_update_date = 0
|
||||
if self.stats and self.stats.last_update_time:
|
||||
self._cached_update_date = self.stats.last_update_time
|
||||
elif self.macro and self.macro.date:
|
||||
# Try to parse the date:
|
||||
try:
|
||||
self._cached_update_date = self._process_date_string_to_python_datetime(
|
||||
self.macro.date
|
||||
)
|
||||
except SyntaxError as e:
|
||||
fci.Console.PrintWarning(str(e) + "\n")
|
||||
else:
|
||||
fci.Console.PrintWarning(f"No update date info for {self.name}\n")
|
||||
return self._cached_update_date
|
||||
|
||||
def _process_date_string_to_python_datetime(self, date_string: str) -> datetime:
|
||||
split_result = re.split(r"[ ./-]+", date_string.strip())
|
||||
print(f"{self.display_name} - {split_result}")
|
||||
if len(split_result) != 3:
|
||||
raise SyntaxError(
|
||||
f"In macro {self.name}, unrecognized date string '{date_string}' (expected YYYY-MM-DD)"
|
||||
)
|
||||
|
||||
if int(split_result[0]) > 2000: # Assume YYYY-MM-DD
|
||||
try:
|
||||
year = int(split_result[0])
|
||||
month = int(split_result[1])
|
||||
day = int(split_result[2])
|
||||
return datetime(year, month, day)
|
||||
except (OverflowError, OSError, ValueError):
|
||||
raise SyntaxError(
|
||||
f"In macro {self.name}, unrecognized date string {date_string} (expected YYYY-MM-DD)"
|
||||
)
|
||||
elif int(split_result[2]) > 2000:
|
||||
# Two possibilities, impossible to distinguish in the general case: DD-MM-YYYY and
|
||||
# MM-DD-YYYY. See if the first one makes sense, and if not, try the second
|
||||
if int(split_result[1]) <= 12:
|
||||
year = int(split_result[2])
|
||||
month = int(split_result[1])
|
||||
day = int(split_result[0])
|
||||
else:
|
||||
year = int(split_result[2])
|
||||
month = int(split_result[0])
|
||||
day = int(split_result[1])
|
||||
return datetime(year, month, day)
|
||||
else:
|
||||
raise SyntaxError(
|
||||
f"In macro {self.name}, unrecognized date string '{date_string}' (expected YYYY-MM-DD)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_macro(cls, macro: Macro):
|
||||
"""Create an Addon object from a Macro wrapper object"""
|
||||
@@ -262,7 +320,8 @@ class Addon:
|
||||
instance = Addon(cache_dict["name"], cache_dict["url"], status, cache_dict["branch"])
|
||||
|
||||
for key, value in cache_dict.items():
|
||||
instance.__dict__[key] = value
|
||||
if not str(key).startswith("_"):
|
||||
instance.__dict__[key] = value
|
||||
|
||||
instance.repo_type = Addon.Kind(cache_dict["repo_type"])
|
||||
if instance.repo_type == Addon.Kind.PACKAGE:
|
||||
@@ -283,6 +342,8 @@ class Addon:
|
||||
instance.python_requires = set(cache_dict["python_requires"])
|
||||
instance.python_optional = set(cache_dict["python_optional"])
|
||||
|
||||
instance._clean_url()
|
||||
|
||||
return instance
|
||||
|
||||
def to_cache(self) -> Dict:
|
||||
@@ -313,6 +374,7 @@ class Addon:
|
||||
if os.path.exists(file):
|
||||
metadata = MetadataReader.from_file(file)
|
||||
self.set_metadata(metadata)
|
||||
self._clean_url()
|
||||
else:
|
||||
fci.Console.PrintLog(f"Internal error: {file} does not exist")
|
||||
|
||||
@@ -338,6 +400,7 @@ class Addon:
|
||||
if url.type == UrlType.repository:
|
||||
self.url = url.location
|
||||
self.branch = url.branch if url.branch else "master"
|
||||
self._clean_url()
|
||||
self.extract_tags(self.metadata)
|
||||
self.extract_metadata_dependencies(self.metadata)
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ from addonmanager_workers_startup import (
|
||||
LoadMacrosFromCacheWorker,
|
||||
CheckWorkbenchesForUpdatesWorker,
|
||||
CacheMacroCodeWorker,
|
||||
GetBasicAddonStatsWorker,
|
||||
)
|
||||
from addonmanager_workers_installation import (
|
||||
UpdateMetadataCacheWorker,
|
||||
@@ -50,12 +51,14 @@ from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
|
||||
from addonmanager_uninstaller_gui import AddonUninstallerGUI
|
||||
from addonmanager_update_all_gui import UpdateAllGUI
|
||||
import addonmanager_utilities as utils
|
||||
import addonmanager_freecad_interface as fci
|
||||
import AddonManager_rc # This is required by Qt, it's not unused
|
||||
from package_list import PackageList, PackageListItemModel
|
||||
from addonmanager_package_details_controller import PackageDetailsController
|
||||
from Widgets.addonmanager_widget_package_details_view import PackageDetailsView
|
||||
from Widgets.addonmanager_widget_global_buttons import WidgetGlobalButtonBar
|
||||
from Addon import Addon
|
||||
from AddonStats import AddonStats
|
||||
from manage_python_dependencies import (
|
||||
PythonPackageManager,
|
||||
)
|
||||
@@ -115,6 +118,7 @@ class CommandAddonManager:
|
||||
"load_macro_metadata_worker",
|
||||
"update_all_worker",
|
||||
"check_for_python_package_updates_worker",
|
||||
"get_basic_addon_stats_worker",
|
||||
]
|
||||
|
||||
lock = threading.Lock()
|
||||
@@ -392,6 +396,7 @@ class CommandAddonManager:
|
||||
self.update_metadata_cache,
|
||||
self.check_updates,
|
||||
self.check_python_updates,
|
||||
self.fetch_addon_stats,
|
||||
]
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
if pref.GetBool("DownloadMacros", False):
|
||||
@@ -660,6 +665,23 @@ class CommandAddonManager:
|
||||
self.manage_python_packages_dialog = PythonPackageManager(self.item_model.repos)
|
||||
self.manage_python_packages_dialog.show()
|
||||
|
||||
def fetch_addon_stats(self) -> None:
|
||||
"""Fetch the Addon Stats JSON data from a URL"""
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
url = pref.GetString("AddonsStatsURL", "https://freecad.org/addon_stats.json")
|
||||
if url and url != "NONE":
|
||||
self.get_basic_addon_stats_worker = GetBasicAddonStatsWorker(
|
||||
url, self.item_model.repos, self.dialog
|
||||
)
|
||||
self.get_basic_addon_stats_worker.finished.connect(self.do_next_startup_phase)
|
||||
self.get_basic_addon_stats_worker.update_addon_stats.connect(self.update_addon_stats)
|
||||
self.get_basic_addon_stats_worker.start()
|
||||
else:
|
||||
self.do_next_startup_phase()
|
||||
|
||||
def update_addon_stats(self, addon: Addon):
|
||||
self.item_model.reload_item(addon)
|
||||
|
||||
def show_developer_tools(self) -> None:
|
||||
"""Display the developer tools dialog"""
|
||||
if not self.developer_mode:
|
||||
|
||||
@@ -26,6 +26,9 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import addonmanager_freecad_interface as fci
|
||||
|
||||
|
||||
def to_int_or_zero(inp: [str | int | None]):
|
||||
@@ -35,11 +38,24 @@ def to_int_or_zero(inp: [str | int | None]):
|
||||
return 0
|
||||
|
||||
|
||||
def time_string_to_datetime(inp: str) -> Optional[datetime]:
|
||||
try:
|
||||
return datetime.fromisoformat(inp)
|
||||
except ValueError:
|
||||
try:
|
||||
# Support for the trailing "Z" was added in Python 3.11 -- strip it and see if it works now
|
||||
return datetime.fromisoformat(inp[:-1])
|
||||
except ValueError:
|
||||
fci.Console.PrintWarning(f"Unable to parse '{str}' as a Python datetime")
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddonStats:
|
||||
"""Statistics about an addon: not all stats apply to all addon types"""
|
||||
|
||||
last_update_time: datetime | None = None
|
||||
date_created: datetime | None = None
|
||||
stars: int = 0
|
||||
open_issues: int = 0
|
||||
forks: int = 0
|
||||
@@ -50,9 +66,16 @@ class AddonStats:
|
||||
def from_json(cls, json_dict: dict):
|
||||
new_stats = AddonStats()
|
||||
if "pushed_at" in json_dict:
|
||||
new_stats.last_update_time = datetime.fromisoformat(json_dict["pushed_at"])
|
||||
new_stats.stars = to_int_or_zero(json_dict["stargazers_count"])
|
||||
new_stats.forks = to_int_or_zero(json_dict["forks_count"])
|
||||
new_stats.open_issues = to_int_or_zero(json_dict["open_issues_count"])
|
||||
new_stats.license = json_dict["license"] # Might be None or "NOASSERTION"
|
||||
new_stats.last_update_time = time_string_to_datetime(json_dict["pushed_at"])
|
||||
if "created_at" in json_dict:
|
||||
new_stats.date_created = time_string_to_datetime(json_dict["created_at"])
|
||||
if "stargazers_count" in json_dict:
|
||||
new_stats.stars = to_int_or_zero(json_dict["stargazers_count"])
|
||||
if "forks_count" in json_dict:
|
||||
new_stats.forks = to_int_or_zero(json_dict["forks_count"])
|
||||
if "open_issues_count" in json_dict:
|
||||
new_stats.open_issues = to_int_or_zero(json_dict["open_issues_count"])
|
||||
if "license" in json_dict:
|
||||
if json_dict["license"] != "NOASSERTION" and json_dict["license"] != "None":
|
||||
new_stats.license = json_dict["license"] # Might be None or "NOASSERTION"
|
||||
return new_stats
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
<file>icons/compact_view.svg</file>
|
||||
<file>icons/composite_view.svg</file>
|
||||
<file>icons/expanded_view.svg</file>
|
||||
<file>icons/sort_ascending.svg</file>
|
||||
<file>icons/sort_descending.svg</file>
|
||||
<file>licenses/Apache-2.0.txt</file>
|
||||
<file>licenses/BSD-2-Clause.txt</file>
|
||||
<file>licenses/BSD-3-Clause.txt</file>
|
||||
|
||||
82
src/Mod/AddonManager/Resources/icons/sort_ascending.svg
Normal file
82
src/Mod/AddonManager/Resources/icons/sort_ascending.svg
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
sodipodi:docname="ascending.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
showgrid="true"
|
||||
inkscape:zoom="64"
|
||||
inkscape:cx="9"
|
||||
inkscape:cy="7.40625"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1368"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
dotted="false"
|
||||
gridanglex="30"
|
||||
gridanglez="30"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs1">
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
inkscape:label="Blur"
|
||||
id="filter2"
|
||||
x="-0.2"
|
||||
y="-0.2"
|
||||
width="1.4"
|
||||
height="1.4">
|
||||
<feGaussianBlur
|
||||
stdDeviation="1 1"
|
||||
result="blur"
|
||||
id="feGaussianBlur2" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
d="m 11,3 v 7 H 10 L 11.5,14.015625 13,10 H 12 V 3 Z"
|
||||
style="fill-rule:evenodd;stroke-width:0.48;stroke-linecap:square;stroke-miterlimit:100;stroke-dashoffset:3.45;fill:#999999;fill-opacity:1"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 5,13.015625 v -7 H 6 L 4.5,2 3,6.015625 h 1 v 7 z"
|
||||
style="fill-rule:evenodd;stroke-width:0.48;stroke-linecap:square;stroke-miterlimit:100;stroke-dashoffset:3.45;fill:#000000;fill-opacity:1"
|
||||
id="path4-0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
82
src/Mod/AddonManager/Resources/icons/sort_descending.svg
Normal file
82
src/Mod/AddonManager/Resources/icons/sort_descending.svg
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
sodipodi:docname="descending.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
showgrid="true"
|
||||
inkscape:zoom="64"
|
||||
inkscape:cx="9"
|
||||
inkscape:cy="7.40625"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1368"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
dotted="false"
|
||||
gridanglex="30"
|
||||
gridanglez="30"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs1">
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
inkscape:label="Blur"
|
||||
id="filter2"
|
||||
x="-0.2"
|
||||
y="-0.2"
|
||||
width="1.4"
|
||||
height="1.4">
|
||||
<feGaussianBlur
|
||||
stdDeviation="1 1"
|
||||
result="blur"
|
||||
id="feGaussianBlur2" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
d="m 11,3 v 7 H 10 L 11.5,14.015625 13,10 H 12 V 3 Z"
|
||||
style="fill-rule:evenodd;stroke-width:0.48;stroke-linecap:square;stroke-miterlimit:100;stroke-dashoffset:3.45"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 5,13.015625 v -7 H 6 L 4.5,2 3,6.015625 h 1 v 7 z"
|
||||
style="fill-rule:evenodd;stroke-width:0.48;stroke-linecap:square;stroke-miterlimit:100;stroke-dashoffset:3.45;fill:#999999;fill-opacity:1"
|
||||
id="path4-0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -23,6 +23,8 @@
|
||||
|
||||
""" Defines a class derived from QWidget for displaying the bar at the top of the addons list. """
|
||||
|
||||
from enum import IntEnum, auto
|
||||
|
||||
# Get whatever version of PySide we can
|
||||
try:
|
||||
import PySide # Use the FreeCAD wrapper
|
||||
@@ -36,11 +38,31 @@ except ImportError:
|
||||
|
||||
PySide = PySide2
|
||||
|
||||
from PySide import QtCore, QtWidgets
|
||||
from PySide import QtCore, QtGui, QtWidgets
|
||||
from .addonmanager_widget_view_selector import WidgetViewSelector
|
||||
from .addonmanager_widget_filter_selector import WidgetFilterSelector
|
||||
from .addonmanager_widget_search import WidgetSearch
|
||||
|
||||
translate = QtCore.QCoreApplication.translate
|
||||
|
||||
|
||||
class SortOptions(IntEnum):
|
||||
_SortRoleOffset = 100
|
||||
Alphabetical = QtCore.Qt.UserRole + _SortRoleOffset + 0
|
||||
LastUpdated = QtCore.Qt.UserRole + _SortRoleOffset + 1
|
||||
DateAdded = QtCore.Qt.UserRole + _SortRoleOffset + 2
|
||||
Stars = QtCore.Qt.UserRole + _SortRoleOffset + 3
|
||||
Rank = QtCore.Qt.UserRole + _SortRoleOffset + 4
|
||||
|
||||
|
||||
default_sort_order = {
|
||||
SortOptions.Alphabetical: QtCore.Qt.AscendingOrder,
|
||||
SortOptions.LastUpdated: QtCore.Qt.DescendingOrder,
|
||||
SortOptions.DateAdded: QtCore.Qt.DescendingOrder,
|
||||
SortOptions.Stars: QtCore.Qt.DescendingOrder,
|
||||
SortOptions.Rank: QtCore.Qt.DescendingOrder,
|
||||
}
|
||||
|
||||
|
||||
class WidgetViewControlBar(QtWidgets.QWidget):
|
||||
"""A bar containing a view selection widget, a filter widget, and a search widget"""
|
||||
@@ -48,28 +70,86 @@ class WidgetViewControlBar(QtWidgets.QWidget):
|
||||
view_changed = QtCore.Signal(int)
|
||||
filter_changed = QtCore.Signal(object)
|
||||
search_changed = QtCore.Signal(str)
|
||||
sort_changed = QtCore.Signal(int)
|
||||
sort_order_changed = QtCore.Signal(QtCore.Qt.SortOrder)
|
||||
|
||||
def __init__(self, parent: QtWidgets.QWidget = None):
|
||||
super().__init__(parent)
|
||||
self._setup_ui()
|
||||
self._setup_connections()
|
||||
self.retranslateUi(None)
|
||||
self.sort_order = QtCore.Qt.AscendingOrder
|
||||
self._set_sort_order_icon()
|
||||
|
||||
def _setup_ui(self):
|
||||
self.horizontal_layout = QtWidgets.QHBoxLayout()
|
||||
self.horizontal_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.view_selector = WidgetViewSelector(self)
|
||||
self.filter_selector = WidgetFilterSelector(self)
|
||||
self.sort_selector = QtWidgets.QComboBox(self)
|
||||
self.sort_order_button = QtWidgets.QToolButton(self)
|
||||
self.sort_order_button.setIcon(
|
||||
QtGui.QIcon.fromTheme("ascending", QtGui.QIcon(":/icons/sort_ascending.svg"))
|
||||
)
|
||||
self.search = WidgetSearch(self)
|
||||
self.horizontal_layout.addWidget(self.view_selector)
|
||||
self.horizontal_layout.addWidget(self.filter_selector)
|
||||
self.horizontal_layout.addWidget(self.sort_selector)
|
||||
self.horizontal_layout.addWidget(self.sort_order_button)
|
||||
self.horizontal_layout.addWidget(self.search)
|
||||
self.setLayout(self.horizontal_layout)
|
||||
|
||||
def _sort_order_clicked(self):
|
||||
if self.sort_order == QtCore.Qt.AscendingOrder:
|
||||
self.set_sort_order(QtCore.Qt.DescendingOrder)
|
||||
else:
|
||||
self.set_sort_order(QtCore.Qt.AscendingOrder)
|
||||
self.sort_order_changed.emit(self.sort_order)
|
||||
|
||||
def set_sort_order(self, order: QtCore.Qt.SortOrder) -> None:
|
||||
self.sort_order = order
|
||||
self._set_sort_order_icon()
|
||||
|
||||
def _set_sort_order_icon(self):
|
||||
if self.sort_order == QtCore.Qt.AscendingOrder:
|
||||
self.sort_order_button.setIcon(
|
||||
QtGui.QIcon.fromTheme(
|
||||
"view-sort-ascending", QtGui.QIcon(":/icons/sort_ascending.svg")
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.sort_order_button.setIcon(
|
||||
QtGui.QIcon.fromTheme(
|
||||
"view-sort-descending", QtGui.QIcon(":/icons/sort_descending.svg")
|
||||
)
|
||||
)
|
||||
|
||||
def _setup_connections(self):
|
||||
self.view_selector.view_changed.connect(self.view_changed.emit)
|
||||
self.filter_selector.filter_changed.connect(self.filter_changed.emit)
|
||||
self.search.search_changed.connect(self.search_changed.emit)
|
||||
self.sort_selector.currentIndexChanged.connect(self._sort_changed)
|
||||
self.sort_order_button.clicked.connect(self._sort_order_clicked)
|
||||
|
||||
def _sort_changed(self, index: int):
|
||||
sort_role = self.sort_selector.itemData(index)
|
||||
self.set_sort_order(default_sort_order[sort_role])
|
||||
self.sort_changed.emit(sort_role)
|
||||
self.sort_order_changed.emit(self.sort_order)
|
||||
|
||||
def retranslateUi(self, _=None):
|
||||
pass
|
||||
self.sort_selector.clear()
|
||||
self.sort_selector.addItem(
|
||||
translate("AddonsInstaller", "Alphabetical", "Sort order"), SortOptions.Alphabetical
|
||||
)
|
||||
self.sort_selector.addItem(
|
||||
translate("AddonsInstaller", "Last Updated", "Sort order"), SortOptions.LastUpdated
|
||||
)
|
||||
self.sort_selector.addItem(
|
||||
translate("AddonsInstaller", "Date Created", "Sort order"), SortOptions.DateAdded
|
||||
)
|
||||
self.sort_selector.addItem(
|
||||
translate("AddonsInstaller", "GitHub Stars", "Sort order"), SortOptions.Stars
|
||||
)
|
||||
# self.sort_selector.addItem(translate("AddonsInstaller", "Rank", "Sort order"),
|
||||
# SortOptions.Rank)
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
# ***************************************************************************
|
||||
|
||||
"""Contains the parser class for extracting metadata from a FreeCAD macro"""
|
||||
import datetime
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
import io
|
||||
import re
|
||||
from typing import Any, Tuple
|
||||
from typing import Any, Tuple, Optional
|
||||
|
||||
try:
|
||||
from PySide import QtCore
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"AddonFlagsURL":
|
||||
"https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/addonflags.json",
|
||||
"AddonsRemoteCacheURL": "https://addons.freecad.org/metadata.zip",
|
||||
"AddonsUpdateStatsURL": "https://addons.freecad.org/addon_update_stats.json",
|
||||
"AddonsStatsURL": "https://freecad.org/addon_stats.json",
|
||||
"AutoCheck": false,
|
||||
"BlockedMacros": "BOLTS,WorkFeatures,how to install,documentation,PartsLibrary,FCGear",
|
||||
"CustomRepoHash": "",
|
||||
|
||||
@@ -41,6 +41,7 @@ import FreeCAD
|
||||
import addonmanager_utilities as utils
|
||||
from addonmanager_macro import Macro
|
||||
from Addon import Addon
|
||||
from AddonStats import AddonStats
|
||||
import NetworkManager
|
||||
from addonmanager_git import initialize_git, GitFailed
|
||||
from addonmanager_metadata import MetadataReader, get_branch_from_metadata
|
||||
@@ -913,3 +914,34 @@ class GetMacroDetailsWorker(QtCore.QThread):
|
||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||
return
|
||||
self.readme_updated.emit(message)
|
||||
|
||||
|
||||
class GetBasicAddonStatsWorker(QtCore.QThread):
|
||||
"""Fetch data from an addon stats repository."""
|
||||
|
||||
update_addon_stats = QtCore.Signal(Addon)
|
||||
|
||||
def __init__(self, url: str, addons: List[Addon], parent: QtCore.QObject = None):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self.addons = addons
|
||||
|
||||
def run(self):
|
||||
"""Fetch the remote data and load it into the addons"""
|
||||
|
||||
fetch_result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(self.url, 5000)
|
||||
if fetch_result is None:
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Failed to get Addon statistics from {} -- only sorting alphabetically will be accurate\n",
|
||||
).format(self.url)
|
||||
)
|
||||
return
|
||||
text_result = fetch_result.data().decode("utf8")
|
||||
json_result = json.loads(text_result)
|
||||
|
||||
for addon in self.addons:
|
||||
if addon.url in json_result:
|
||||
addon.stats = AddonStats.from_json(json_result[addon.url])
|
||||
self.update_addon_stats.emit(addon)
|
||||
|
||||
@@ -72,6 +72,11 @@ class Ui_ExpandedView(object):
|
||||
|
||||
self.horizontalLayout.addWidget(self.labelTags)
|
||||
|
||||
self.labelSort = QLabel(ExpandedView)
|
||||
self.labelSort.setObjectName("labelSort")
|
||||
|
||||
self.horizontalLayout.addWidget(self.labelSort)
|
||||
|
||||
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
|
||||
self.horizontalLayout.addItem(self.horizontalSpacer_2)
|
||||
|
||||
@@ -122,6 +122,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelSort">
|
||||
<property name="text">
|
||||
<string>labelSort</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -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