From 5de3fe384b5ce6a1eb155052bbd04c65965ccb57 Mon Sep 17 00:00:00 2001 From: PhoneDroid <73050054+PhoneDroid@users.noreply.github.com> Date: Sun, 4 Jan 2026 04:52:01 -0500 Subject: [PATCH] Add Contains Filter Option --- Addon.py | 14 ++++ .../addonmanager_widget_filter_selector.py | 74 ++++++++++++++++--- addonmanager_metadata.py | 21 ++++++ addonmanager_preferences_defaults.json | 1 + package_list.py | 29 +++++++- 5 files changed, 129 insertions(+), 10 deletions(-) diff --git a/Addon.py b/Addon.py index 26de1d78..71d5ca89 100644 --- a/Addon.py +++ b/Addon.py @@ -491,6 +491,20 @@ def contains_other(self) -> bool: """Determine if this package contains an "other" content item""" return self.contains_packaged_content("other") + def contains_no_generated_content(self) -> bool: + """Determines if this package explicitly contains no generated content""" + + if self.repo_type != Addon.Kind.PACKAGE: + return False + + if not self.metadata: + return False + + if not self.metadata.contains: + return False + + return self.metadata.contains.generated_content == False + def walk_dependency_tree(self, all_repos: Dict[str, "Addon"], deps: Dependencies): """Compute the total dependency tree for this repo (recursive) - all_repos is a dictionary of repos, keyed on the name of the repo diff --git a/Widgets/addonmanager_widget_filter_selector.py b/Widgets/addonmanager_widget_filter_selector.py index e3754d74..ab81e6a5 100644 --- a/Widgets/addonmanager_widget_filter_selector.py +++ b/Widgets/addonmanager_widget_filter_selector.py @@ -24,6 +24,7 @@ """Defines a QWidget-derived class for displaying the view selection buttons.""" from enum import IntEnum +from typing import Set from addonmanager_freecad_interface import translate @@ -35,6 +36,7 @@ class FilterType(IntEnum): PACKAGE_CONTENTS = 0 INSTALLATION_STATUS = 1 + CONTAINS = 2 class StatusFilter(IntEnum): @@ -57,10 +59,16 @@ class ContentFilter(IntEnum): OTHER = 5 +class ContainsFilter(IntEnum): + + NO_GENERATED_CONTENT = 0 + + class Filter: def __init__(self): self.status_filter = StatusFilter.ANY self.content_filter = ContentFilter.ANY + self.contains_filter: Set[ContainsFilter] = set() class WidgetFilterSelector(QtWidgets.QComboBox): @@ -71,6 +79,7 @@ class WidgetFilterSelector(QtWidgets.QComboBox): def __init__(self, parent: QtWidgets.QWidget = None): super().__init__(parent) self.addon_type_index = 0 + self.contains_index = 0 self.installation_status_index = 0 self.extra_padding = 64 self._setup_ui() @@ -128,6 +137,13 @@ def _build_menu(self): translate("AddonsInstaller", "Update available"), (FilterType.INSTALLATION_STATUS, StatusFilter.UPDATE_AVAILABLE), ) + self.insertSeparator(self.count()) + self.addItem(translate("AddonsInstaller", "Contains")) + self.contains_index = self.count() - 1 + self.addItem( + translate("AddonsInstaller", "No generated content"), + (FilterType.CONTAINS, ContainsFilter.NO_GENERATED_CONTENT), + ) model: QtCore.QAbstractItemModel = self.model() for row in range(model.rowCount()): if row <= self.addon_type_index: @@ -137,6 +153,11 @@ def _build_menu(self): item.setCheckState(QtCore.Qt.Unchecked) elif row == self.installation_status_index: model.item(row).setEnabled(False) + elif row < self.contains_index: + item = model.item(row) + item.setCheckState(QtCore.Qt.Unchecked) + elif row == self.contains_index: + model.item(row).setEnabled(False) else: item = model.item(row) item.setCheckState(QtCore.Qt.Unchecked) @@ -176,6 +197,18 @@ def set_status_filter(self, status_filter: StatusFilter): item.setCheckState(QtCore.Qt.Unchecked) self._update_first_row_text() + def set_contains_filter(self, filter: Set[ContainsFilter]): + model = self.model() + for row in range(model.rowCount()): + item = model.item(row) + user_data = self.itemData(row) + if user_data and user_data[0] == FilterType.CONTAINS: + if user_data[1] in filter: + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + self._update_first_row_text() + def _setup_connections(self): self.activated.connect(self._selected) @@ -194,7 +227,11 @@ def retranslateUi(self, _): def _selected(self, row: int): if row == 0: return - if row == self.installation_status_index or row == self.addon_type_index: + if ( + row == self.installation_status_index + or row == self.addon_type_index + or row == self.contains_index + ): self.setCurrentIndex(0) return model = self.model() @@ -207,10 +244,18 @@ def _selected(self, row: int): item = model.item(row) user_data = self.itemData(row) if user_data and user_data[0] == selected_row_type: - if user_data[1] == selected_data[1]: - item.setCheckState(QtCore.Qt.Checked) + + if selected_row_type == FilterType.CONTAINS: + if user_data[1] == selected_data[1]: + if item.checkState() == QtCore.Qt.Checked: + item.setCheckState(QtCore.Qt.Unchecked) + else: + item.setCheckState(QtCore.Qt.Checked) else: - item.setCheckState(QtCore.Qt.Unchecked) + if user_data[1] == selected_data[1]: + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) self._emit_current_filter() self.setCurrentIndex(0) self._update_first_row_text() @@ -221,11 +266,22 @@ def _emit_current_filter(self): for row in range(model.rowCount()): item = model.item(row) data = self.itemData(row) - if data and item.checkState() == QtCore.Qt.Checked: - if data[0] == FilterType.INSTALLATION_STATUS: - new_filter.status_filter = data[1] - elif data[0] == FilterType.PACKAGE_CONTENTS: - new_filter.content_filter = data[1] + if data: + + if item.checkState() == QtCore.Qt.Checked: + if data[0] == FilterType.INSTALLATION_STATUS: + new_filter.status_filter = data[1] + elif data[0] == FilterType.PACKAGE_CONTENTS: + new_filter.content_filter = data[1] + + if data[0] == FilterType.CONTAINS: + if item.checkState() == QtCore.Qt.Checked: + if data[1] not in new_filter.contains_filter: + new_filter.contains_filter.add(data[1]) + else: + if data[1] in new_filter.contains_filter: + new_filter.contains_filter.remove(data[1]) + self.filter_changed.emit(new_filter) def _update_first_row_text(self): diff --git a/addonmanager_metadata.py b/addonmanager_metadata.py index b5cb0f49..53009ed8 100644 --- a/addonmanager_metadata.py +++ b/addonmanager_metadata.py @@ -40,6 +40,11 @@ import xml.etree.ElementTree as ET +@dataclass +class Contains: + generated_content: None | bool = True + + @dataclass class Contact: name: str @@ -215,6 +220,7 @@ class Metadata: freecadmax: Version = None pythonmin: Version = None content: Dict[str, List[Metadata]] = field(default_factory=dict) # Recursive def. + contains: None | Contains = None def get_first_supported_freecad_version(metadata: Metadata) -> Optional[Version]: @@ -313,6 +319,8 @@ def _parse_child_element(namespace: str, child: ET.Element, metadata: Metadata): metadata.__dict__[tag].append(MetadataReader._parse_dependency(child)) elif tag == "content": MetadataReader._parse_content(namespace, metadata, child) + elif tag == "contains": + metadata.contains = MetadataReader._parse_contains(child, metadata.name) @staticmethod def _parse_contact(child: ET.Element) -> Contact: @@ -387,6 +395,19 @@ def _create_node(namespace, child) -> Metadata: MetadataReader._parse_child_element(namespace, content_child, new_content_item) return new_content_item + @staticmethod + def _parse_contains(child: ET.Element, name: str) -> Contains: + value = child.attrib["generated_content"] if "generated_content" in child.attrib else None + + state = None + + if value in ["true", "false"]: + state = value == "true" + else: + print(f"Unrecognized value ( '{ value }' ) for generated_content for addon '{ name }'") + + return Contains(generated_content=state) + class MetadataWriter: """Utility class for serializing a Metadata object into the package.xml standard diff --git a/addonmanager_preferences_defaults.json b/addonmanager_preferences_defaults.json index 3e395c7a..fd775fbb 100644 --- a/addonmanager_preferences_defaults.json +++ b/addonmanager_preferences_defaults.json @@ -15,6 +15,7 @@ "MacroUpdateStatsURL": "https://addons.freecad.org/macro_update_stats.json", "NoProxyCheck": false, "PackageTypeSelection": 0, + "ContainsSelection": "" , "ProxyUrl": "", "SearchString": "", "SelectedAddon": "", diff --git a/package_list.py b/package_list.py index c82c30ad..7f2cf1c9 100644 --- a/package_list.py +++ b/package_list.py @@ -23,6 +23,7 @@ """Defines the PackageList QWidget for displaying a list of Addons.""" import threading +from typing import Set import addonmanager_freecad_interface as fci from PySideWrapper import QtCore, QtGui, QtWidgets @@ -35,7 +36,7 @@ import addonmanager_utilities as utils 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_filter_selector import StatusFilter, Filter, ContainsFilter from Widgets.addonmanager_widget_progress_bar import Progress, WidgetProgressBar from addonmanager_licenses import get_license_manager @@ -73,12 +74,21 @@ def __init__(self, parent=None): # Set up the view the same as the last time: package_type = fci.Preferences().get("PackageTypeSelection") + contains: str = fci.Preferences().get("ContainsSelection") status = fci.Preferences().get("StatusSelection") search_string = fci.Preferences().get("SearchString") + + contains_filter = set() + + if len(contains) > 0: + contains_filter = set([ContainsFilter(int(value)) for value in contains.split(",")]) + self.ui.view_bar.filter_selector.set_contents_filter(package_type) + self.ui.view_bar.filter_selector.set_contains_filter(contains_filter) 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.setContainsFilter(contains_filter) self.item_filter.setPackageFilter(package_type) self.item_filter.setStatusFilter(status) @@ -126,8 +136,13 @@ def update_status_filter(self, new_filter: Filter) -> None: self.item_filter.setStatusFilter(new_filter.status_filter) self.item_filter.setPackageFilter(new_filter.content_filter) + self.item_filter.setContainsFilter(new_filter.contains_filter) + + contains = ",".join([str(value) for value in new_filter.contains_filter]) + fci.Preferences().set("StatusSelection", new_filter.status_filter) fci.Preferences().set("PackageTypeSelection", new_filter.content_filter) + fci.Preferences().set("ContainsSelection", contains) self.item_filter.invalidateFilter() def set_view_style(self, style: AddonManagerDisplayStyle) -> None: @@ -573,11 +588,15 @@ def paint( class PackageListFilter(QtCore.QSortFilterProxyModel): + + contains: Set[ContainsFilter] + """Handle filtering the item list on various criteria""" def __init__(self): super().__init__() self.package_type = 0 # Default to showing everything + self.contains = set() self.status = 0 # Default to showing any self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) self.hide_non_OSI_approved = False @@ -592,6 +611,10 @@ def setPackageFilter( self.package_type = package_type self.invalidateFilter() + def setContainsFilter(self, filter: Set[ContainsFilter]) -> None: + self.contains = filter + self.invalidateFilter() + def setStatusFilter( self, status: int ) -> None: # 0=Any, 1=Installed, 2=Not installed, 3=Update available @@ -645,6 +668,10 @@ def filterAcceptsRow(self, row, _parent=QtCore.QModelIndex()): if data.status() != Addon.Status.UPDATE_AVAILABLE: return False + if ContainsFilter.NO_GENERATED_CONTENT in self.contains: + if not data.contains_no_generated_content(): + return False + license_manager = get_license_manager() if data.status() == Addon.Status.NOT_INSTALLED: