diff --git a/src/Mod/AddonManager/Addon.py b/src/Mod/AddonManager/Addon.py
index d028429689..8ec60ce48e 100644
--- a/src/Mod/AddonManager/Addon.py
+++ b/src/Mod/AddonManager/Addon.py
@@ -489,23 +489,17 @@ class Addon:
if self.repo_type == Addon.Kind.WORKBENCH:
return True
- if self.repo_type == Addon.Kind.PACKAGE:
- if self.metadata is None:
- fci.Console.PrintLog(
- f"Addon Manager internal error: lost metadata for package {self.name}\n"
- )
- return False
- content = self.metadata.content
- if not content:
- return False
- return "workbench" in content
- return False
+ return self.contains_packaged_content("workbench")
def contains_macro(self) -> bool:
"""Determine if this package contains (or is) a macro"""
if self.repo_type == Addon.Kind.MACRO:
return True
+ return self.contains_packaged_content("macro")
+
+ def contains_packaged_content(self, content_type: str):
+ """Determine if the package contains content_type"""
if self.repo_type == Addon.Kind.PACKAGE:
if self.metadata is None:
fci.Console.PrintLog(
@@ -513,21 +507,20 @@ class Addon:
)
return False
content = self.metadata.content
- return "macro" in content
+ return content_type in content
return False
def contains_preference_pack(self) -> bool:
"""Determine if this package contains a preference pack"""
+ return self.contains_packaged_content("preferencepack")
- if self.repo_type == Addon.Kind.PACKAGE:
- if self.metadata is None:
- fci.Console.PrintLog(
- f"Addon Manager internal error: lost metadata for package {self.name}\n"
- )
- return False
- content = self.metadata.content
- return "preferencepack" in content
- return False
+ def contains_bundle(self) -> bool:
+ """Determine if this package contains a bundle"""
+ return self.contains_packaged_content("bundle")
+
+ def contains_other(self) -> bool:
+ """Determine if this package contains an "other" content item"""
+ return self.contains_packaged_content("other")
def get_best_icon_relative_path(self) -> str:
"""Get the path within the repo the addon's icon. Usually specified by
diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py b/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py
index e3a20d33d4..3872364739 100644
--- a/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py
+++ b/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py
@@ -93,6 +93,8 @@ class TestAddon(unittest.TestCase):
self.assertTrue(addon_with_workbench.contains_workbench())
self.assertFalse(addon_with_workbench.contains_macro())
self.assertFalse(addon_with_workbench.contains_preference_pack())
+ self.assertFalse(addon_with_workbench.contains_bundle())
+ self.assertFalse(addon_with_workbench.contains_other())
# Macros
addon_with_macro = Addon(
@@ -105,6 +107,8 @@ class TestAddon(unittest.TestCase):
self.assertFalse(addon_with_macro.contains_workbench())
self.assertTrue(addon_with_macro.contains_macro())
self.assertFalse(addon_with_macro.contains_preference_pack())
+ self.assertFalse(addon_with_workbench.contains_bundle())
+ self.assertFalse(addon_with_workbench.contains_other())
# Preference Packs
addon_with_prefpack = Addon(
@@ -117,6 +121,8 @@ class TestAddon(unittest.TestCase):
self.assertFalse(addon_with_prefpack.contains_workbench())
self.assertFalse(addon_with_prefpack.contains_macro())
self.assertTrue(addon_with_prefpack.contains_preference_pack())
+ self.assertFalse(addon_with_workbench.contains_bundle())
+ self.assertFalse(addon_with_workbench.contains_other())
# Combination
addon_with_all = Addon(
@@ -129,6 +135,8 @@ class TestAddon(unittest.TestCase):
self.assertTrue(addon_with_all.contains_workbench())
self.assertTrue(addon_with_all.contains_macro())
self.assertTrue(addon_with_all.contains_preference_pack())
+ self.assertTrue(addon_with_all.contains_bundle())
+ self.assertTrue(addon_with_all.contains_other())
# Now do the simple, explicitly-set cases
addon_wb = Addon(
diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_metadata.py b/src/Mod/AddonManager/AddonManagerTest/app/test_metadata.py
index b572327d79..9f6f53cecf 100644
--- a/src/Mod/AddonManager/AddonManagerTest/app/test_metadata.py
+++ b/src/Mod/AddonManager/AddonManagerTest/app/test_metadata.py
@@ -616,6 +616,22 @@ class TestMetadataReaderIntegration(unittest.TestCase):
expected_packs.remove(wb.name)
self.assertEqual(len(expected_packs), 0)
+ def test_bundle(self):
+ from addonmanager_metadata import MetadataReader
+
+ filename = os.path.join(self.test_data_dir, "bundle_only.xml")
+ metadata = MetadataReader.from_file(filename)
+ self.assertIn("bundle", metadata.content)
+ self.assertEqual(len(metadata.content["bundle"]), 1)
+
+ def test_other(self):
+ from addonmanager_metadata import MetadataReader
+
+ filename = os.path.join(self.test_data_dir, "other_only.xml")
+ metadata = MetadataReader.from_file(filename)
+ self.assertIn("other", metadata.content)
+ self.assertEqual(len(metadata.content["other"]), 1)
+
def test_content_combination(self):
from addonmanager_metadata import MetadataReader
diff --git a/src/Mod/AddonManager/AddonManagerTest/data/bundle_only.xml b/src/Mod/AddonManager/AddonManagerTest/data/bundle_only.xml
new file mode 100644
index 0000000000..0cac57f292
--- /dev/null
+++ b/src/Mod/AddonManager/AddonManagerTest/data/bundle_only.xml
@@ -0,0 +1,23 @@
+
+
+ Test Bundle
+ A package.xml file for unit testing.
+ 1.0.0
+ 2025-02-22
+ FreeCAD Developer
+ LGPL-2.1
+ https://github.com/chennes/FreeCAD-Package
+ https://github.com/chennes/FreeCAD-Package/blob/main/README.md
+
+
+
+ A bunch of great addons you should install
+ TestAddon1
+ TestAddon2
+ TestAddon3
+ TestAddon4
+ TestAddon5
+
+
+
+
diff --git a/src/Mod/AddonManager/AddonManagerTest/data/combination.xml b/src/Mod/AddonManager/AddonManagerTest/data/combination.xml
index 8f095046f5..bc72470a0e 100644
--- a/src/Mod/AddonManager/AddonManagerTest/data/combination.xml
+++ b/src/Mod/AddonManager/AddonManagerTest/data/combination.xml
@@ -23,6 +23,12 @@
MyFirstPack
+
+ A bundle that bundles nothing
+
+
+ Mysterious Object
+
diff --git a/src/Mod/AddonManager/AddonManagerTest/data/other_only.xml b/src/Mod/AddonManager/AddonManagerTest/data/other_only.xml
new file mode 100644
index 0000000000..e4401f334d
--- /dev/null
+++ b/src/Mod/AddonManager/AddonManagerTest/data/other_only.xml
@@ -0,0 +1,18 @@
+
+
+ Test Other
+ A package.xml file for unit testing.
+ 1.0.0
+ 2025-02-22
+ FreeCAD Developer
+ LGPL-2.1
+ https://github.com/chennes/FreeCAD-Package
+ https://github.com/chennes/FreeCAD-Package/blob/main/README.md
+
+
+
+ A thing that's not a workbench, macro, preference pack, or bundle
+
+
+
+
diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index 58b15d132b..8efcf67a0e 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -118,6 +118,7 @@ SET(AddonManagerTestsGui_SRCS
SET(AddonManagerTestsFiles_SRCS
AddonManagerTest/data/__init__.py
AddonManagerTest/data/addon_update_stats.json
+ AddonManagerTest/data/bundle_only.xml
AddonManagerTest/data/combination.xml
AddonManagerTest/data/corrupted_metadata.zip
AddonManagerTest/data/depends_on_all_workbenches.xml
@@ -131,6 +132,7 @@ SET(AddonManagerTestsFiles_SRCS
AddonManagerTest/data/MacrosRecipesWikiPage.zip
AddonManagerTest/data/metadata.zip
AddonManagerTest/data/missing_macro_metadata.FCStd
+ AddonManagerTest/data/other_only.xml
AddonManagerTest/data/prefpack_only.xml
AddonManagerTest/data/test_addon_with_fcmacro.zip
AddonManagerTest/data/test_github_style_repo.zip
diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py
index 308335e3c9..bb07ee3a28 100644
--- a/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py
+++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py
@@ -69,6 +69,8 @@ class ContentFilter(IntEnum):
WORKBENCH = 1
MACRO = 2
PREFERENCE_PACK = 3
+ BUNDLE = 4
+ OTHER = 5
class Filter:
@@ -116,6 +118,14 @@ class WidgetFilterSelector(QtWidgets.QComboBox):
translate("AddonsInstaller", "Preference Pack"),
(FilterType.PACKAGE_CONTENTS, ContentFilter.PREFERENCE_PACK),
)
+ self.addItem(
+ translate("AddonsInstaller", "Bundle"),
+ (FilterType.PACKAGE_CONTENTS, ContentFilter.BUNDLE),
+ )
+ self.addItem(
+ translate("AddonsInstaller", "Other"),
+ (FilterType.PACKAGE_CONTENTS, ContentFilter.OTHER),
+ )
self.insertSeparator(self.count())
self.addItem(translate("AddonsInstaller", "Installation Status"))
self.installation_status_index = self.count() - 1
diff --git a/src/Mod/AddonManager/addonmanager_devmode_add_content.py b/src/Mod/AddonManager/addonmanager_devmode_add_content.py
index f990f643ec..2907fbc8d4 100644
--- a/src/Mod/AddonManager/addonmanager_devmode_add_content.py
+++ b/src/Mod/AddonManager/addonmanager_devmode_add_content.py
@@ -71,6 +71,8 @@ class AddContent:
self.dialog.addonKindComboBox.setItemData(0, "macro")
self.dialog.addonKindComboBox.setItemData(1, "preferencepack")
self.dialog.addonKindComboBox.setItemData(2, "workbench")
+ self.dialog.addonKindComboBox.setItemData(3, "bundle")
+ self.dialog.addonKindComboBox.setItemData(4, "other")
self.people_table = PeopleTable()
self.licenses_table = LicensesTable()
@@ -148,6 +150,8 @@ class AddContent:
self.dialog.macroFileLineEdit.setText(files[0])
elif addon_kind == "preferencepack":
self.dialog.prefPackNameLineEdit.setText(self.metadata.Name)
+ elif addon_kind == "bundle" or addon_kind == "other":
+ pass
else:
raise RuntimeError("Invalid data found for selection")
diff --git a/src/Mod/AddonManager/addonmanager_metadata.py b/src/Mod/AddonManager/addonmanager_metadata.py
index debe1bac27..d72f2b92c1 100644
--- a/src/Mod/AddonManager/addonmanager_metadata.py
+++ b/src/Mod/AddonManager/addonmanager_metadata.py
@@ -367,7 +367,7 @@ class MetadataReader:
def _parse_content(namespace: str, metadata: Metadata, root: ET.Element):
"""Given a content node, loop over its children, and if they are a recognized
element type, recurse into each one to parse it."""
- known_content_types = ["workbench", "macro", "preferencepack"]
+ known_content_types = ["workbench", "macro", "preferencepack", "bundle", "other"]
for child in root:
content_type = child.tag[len(namespace) :]
if content_type in known_content_types:
diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py
index b4590b776f..e946e14f12 100644
--- a/src/Mod/AddonManager/package_list.py
+++ b/src/Mod/AddonManager/package_list.py
@@ -569,7 +569,7 @@ class PackageListFilter(QtCore.QSortFilterProxyModel):
def setPackageFilter(
self, package_type: int
- ) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs
+ ) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs, 4=Bundles, 5=Other
"""Set the package filter to package_type and refreshes."""
self.package_type = package_type
self.invalidateFilter()
@@ -634,6 +634,12 @@ class PackageListFilter(QtCore.QSortFilterProxyModel):
elif self.package_type == 3:
if not data.contains_preference_pack():
return False
+ elif self.package_type == 4:
+ if not data.contains_bundle():
+ return False
+ elif self.package_type == 5:
+ if not data.contains_other():
+ return False
if self.status == StatusFilter.INSTALLED:
if data.status() == Addon.Status.NOT_INSTALLED: