diff --git a/AddonCatalog.py b/AddonCatalog.py
index 50a5dcc0..c35daa6a 100644
--- a/AddonCatalog.py
+++ b/AddonCatalog.py
@@ -233,9 +233,6 @@ def _load_metadata_txt(repo: Addon, data: str):
wb_name = wb.strip()
if wb_name:
repo.requires.add(wb_name)
- fci.Console.PrintLog(
- f"{repo.display_name} requires FreeCAD addon '{wb_name}'\n"
- )
elif line.startswith("pylibs="):
python_dependencies = line.split("=")[1].split(",")
@@ -243,9 +240,6 @@ def _load_metadata_txt(repo: Addon, data: str):
dep = pl.strip()
if dep:
repo.python_requires.add(dep)
- fci.Console.PrintLog(
- f"{repo.display_name} requires python package '{dep}'\n"
- )
elif line.startswith("optionalpylibs="):
optional_python_dependencies = line.split("=")[1].split(",")
@@ -253,10 +247,6 @@ def _load_metadata_txt(repo: Addon, data: str):
dep = pl.strip()
if dep:
repo.python_optional.add(dep)
- fci.Console.PrintLog(
- f"{repo.display_name} optionally imports python package"
- + f" '{pl.strip()}'\n"
- )
@staticmethod
def _load_requirements_txt(repo: Addon, data: str):
diff --git a/AddonManager.py b/AddonManager.py
index f55c1ea9..42a516c1 100644
--- a/AddonManager.py
+++ b/AddonManager.py
@@ -29,7 +29,7 @@
import threading
from typing import Dict, List
-from PySideWrapper import QtGui, QtCore, QtWidgets, QtSvg
+from PySideWrapper import QtGui, QtCore, QtWidgets
from addonmanager_workers_startup import (
CreateAddonListWorker,
@@ -38,6 +38,7 @@
GetAddonScoreWorker,
)
from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
+from addonmanager_icon_utilities import get_icon_for_addon
from addonmanager_uninstaller_gui import AddonUninstallerGUI
from addonmanager_update_all_gui import UpdateAllGUI
import addonmanager_utilities as utils
@@ -92,84 +93,6 @@ def QT_TRANSLATE_NOOP(_, txt):
INSTANCE = None
-class SvgIconEngine(QtGui.QIconEngine):
- def __init__(self, svg_bytes: bytes):
- super().__init__()
- self.renderer = QtSvg.QSvgRenderer(QtCore.QByteArray(svg_bytes))
-
- def paint(self, painter: QtGui.QPainter, rect: QtCore.QRect, mode, state):
- self.renderer.render(painter, rect)
-
- def pixmap(self, size: QtCore.QSize, mode, state):
- pixmap = QtGui.QPixmap(size)
- pixmap.fill(QtCore.Qt.transparent)
- painter = QtGui.QPainter(pixmap)
- self.renderer.render(painter)
- painter.end()
- return pixmap
-
-
-def scalable_icon_from_svg_bytes(svg_bytes: bytes) -> QtGui.QIcon:
- engine = SvgIconEngine(svg_bytes)
- return QtGui.QIcon(engine)
-
-
-def get_icon(repo: Addon, update: bool = False) -> QtGui.QIcon:
- """Returns an icon for an Addon. Uses a cached icon if possible, unless `update` is True,
- in which case the icon is regenerated."""
-
- icon_path = os.path.join(os.path.dirname(__file__), "Resources", "icons")
- path = ""
-
- if not update:
- if repo.icon and not repo.icon.isNull() and repo.icon.isValid():
- return repo.icon
- elif repo.icon_data:
- repo.icon = scalable_icon_from_svg_bytes(repo.icon_data)
- return repo.icon
-
- default_icon = QtGui.QIcon(os.path.join(icon_path, "document-package.svg"))
- if repo.repo_type == Addon.Kind.WORKBENCH:
- default_icon = QtGui.QIcon(os.path.join(icon_path, "document-package.svg"))
- elif repo.repo_type == Addon.Kind.MACRO:
- if repo.macro and repo.macro.icon_data:
- if repo.macro.icon_extension == "svg":
- default_icon = scalable_icon_from_svg_bytes(repo.macro.icon_data)
- else:
- pixmap = QtGui.QPixmap()
- pixmap.loadFromData(repo.macro.icon_data)
- default_icon = QtGui.QIcon(pixmap)
- elif repo.macro and repo.macro.xpm:
- cache_path = fci.DataPaths().cache_dir
- am_path = os.path.join(cache_path, "AddonManager", "MacroIcons")
- os.makedirs(am_path, exist_ok=True)
- path = os.path.join(am_path, repo.name + "_icon.xpm")
- if not os.path.exists(path):
- with open(path, "w") as f:
- f.write(repo.macro.xpm)
- default_icon = QtGui.QIcon(repo.macro.xpm)
- else:
- default_icon = QtGui.QIcon(os.path.join(icon_path, "document-python.svg"))
- elif repo.repo_type == Addon.Kind.PACKAGE:
- # The cache might not have been downloaded yet, check to see if it's there...
- if repo.icon_data:
- default_icon = scalable_icon_from_svg_bytes(repo.icon_data)
- elif repo.contains_workbench():
- default_icon = QtGui.QIcon(os.path.join(icon_path, "document-package.svg"))
- elif repo.contains_macro():
- default_icon = QtGui.QIcon(os.path.join(icon_path, "document-python.svg"))
- else:
- default_icon = QtGui.QIcon(os.path.join(icon_path, "document-package.svg"))
-
- if QtCore.QFile.exists(path):
- addon_icon = QtGui.QIcon(path)
- else:
- addon_icon = default_icon
- repo.icon = addon_icon
-
- return addon_icon
-
-
class CommandAddonManager(QtCore.QObject):
"""The main Addon Manager class and FreeCAD command"""
@@ -456,7 +379,7 @@ def on_package_updated(self, repo: Addon) -> None:
"""Called when the named package has either new metadata or a new icon (or both)"""
with self.lock:
- repo.icon = get_icon(repo, update=True)
+ repo.icon = get_icon_for_addon(repo, update=True)
self.item_model.reload_item(repo)
def select_addon(self) -> None:
@@ -593,7 +516,7 @@ def add_addon_repo(self, addon_repo: Addon) -> None:
"""adds a workbench to the list"""
if addon_repo.icon is None or addon_repo.icon.isNull():
- addon_repo.icon = get_icon(addon_repo)
+ addon_repo.icon = get_icon_for_addon(addon_repo)
for repo in self.item_model.repos:
if repo.name == addon_repo.name:
# self.item_model.reload_item(repo) # If we want to have later additions superseded
diff --git a/AddonManagerTest/gui/test_icon_utilities.py b/AddonManagerTest/gui/test_icon_utilities.py
new file mode 100644
index 00000000..750f0a49
--- /dev/null
+++ b/AddonManagerTest/gui/test_icon_utilities.py
@@ -0,0 +1,435 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+import gzip
+import io
+import unittest
+from types import SimpleNamespace
+from unittest.mock import patch
+
+import addonmanager_icon_utilities as iu
+
+from PySideWrapper import QtCore, QtGui
+
+
+class TestIsValidXML(unittest.TestCase):
+ """Test the icon utilities. Many of these must be run with a QApplication running because the
+ functions use some features of QIcon that require it."""
+
+ def test_is_valid_xml_good_inputs(self):
+ valid_xml_entries = [
+ b"",
+ b'',
+ b'',
+ b'',
+ b'text',
+ "".encode("utf-8"),
+ "parsed but kept as text]]>".encode("utf-8"),
+ (
+ ""
+ ""
+ ).encode("utf-8"),
+ ]
+ for entry in valid_xml_entries:
+ with self.subTest(entry=entry):
+ self.assertTrue(iu.is_valid_xml(entry))
+
+ def test_is_valid_xml_bad_inputs(self):
+ invalid_xml_entries = [
+ b"", # empty -> ParseError
+ b"