- #
- #
- #
-
-
-# @}
diff --git a/src/Mod/AddonManager/addonmanager_macro_parser.py b/src/Mod/AddonManager/addonmanager_macro_parser.py
deleted file mode 100644
index 8a560c79ae..0000000000
--- a/src/Mod/AddonManager/addonmanager_macro_parser.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2023 FreeCAD Project Association *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""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, Optional
-
-try:
- from PySide import QtCore
-except ImportError:
- QtCore = None
-
-try:
- import FreeCAD
- from addonmanager_licenses import get_license_manager
-except ImportError:
- FreeCAD = None
- get_license_manager = None
-
-
-class DummyThread:
- @classmethod
- def isInterruptionRequested(cls):
- return False
-
-
-class MacroParser:
- """Extracts metadata information from a FreeCAD macro"""
-
- MAX_LINES_TO_SEARCH = 200 # To speed up parsing: some files are VERY large
-
- def __init__(self, name: str, code: str = ""):
- """Create a parser for the macro named "name". Note that the name is only
- used as the context for error messages, it is not otherwise important."""
- self.name = name
- self.parse_results = {
- "comment": "",
- "url": "",
- "wiki": "",
- "version": "",
- "other_files": [""],
- "author": "",
- "date": "",
- "license": "",
- "icon": "",
- "xpm": "",
- }
- self.remaining_item_map = {}
- self.console = None if FreeCAD is None else FreeCAD.Console
- self.current_thread = DummyThread() if QtCore is None else QtCore.QThread.currentThread()
- if code:
- self.fill_details_from_code(code)
-
- def _reset_map(self):
- """This map tracks which items we've already read. If the same parser is used
- twice, it has to be reset."""
- self.remaining_item_map = {
- "__comment__": "comment",
- "__web__": "url",
- "__wiki__": "wiki",
- "__version__": "version",
- "__files__": "other_files",
- "__author__": "author",
- "__date__": "date",
- "__license__": "license",
- "__licence__": "license", # accept either spelling
- "__icon__": "icon",
- "__xpm__": "xpm",
- }
-
- def fill_details_from_code(self, code: str) -> None:
- """Reads in the macro code from the given string and parses it for its
- metadata."""
-
- self._reset_map()
- line_counter = 0
- content_lines = io.StringIO(code)
- while content_lines and line_counter < self.MAX_LINES_TO_SEARCH:
- line = content_lines.readline()
- if not line:
- break
- if self.current_thread.isInterruptionRequested():
- return
- line_counter += 1
- if not line.startswith("__"):
- # Speed things up a bit... this comparison is very cheap
- continue
- try:
- self._process_line(line, content_lines)
- except SyntaxError as e:
- err_string = f"Syntax error when parsing macro {self.name}:\n{str(e)}"
- if self.console:
- self.console.PrintWarning(err_string)
- else:
- print(err_string)
-
- def _process_line(self, line: str, content_lines: io.StringIO):
- """Given a single line of the macro file, see if it matches one of our items,
- and if so, extract the data."""
-
- lowercase_line = line.lower()
- for key in self.remaining_item_map:
- if lowercase_line.startswith(key):
- self._process_key(key, line, content_lines)
- break
-
- def _process_key(self, key: str, line: str, content_lines: io.StringIO):
- """Given a line that starts with a known key, extract the data for that key,
- possibly reading in additional lines (if it contains a line continuation
- character, or is a triple-quoted string)."""
-
- line = self._handle_backslash_continuation(line, content_lines)
- line, was_triple_quoted = self._handle_triple_quoted_string(line, content_lines)
-
- _, _, line = line.partition("=")
- if not was_triple_quoted:
- line, _, _ = line.partition("#")
- self._detect_illegal_content(line)
- final_content_line = line.strip()
-
- stripped_of_quotes = self._strip_quotes(final_content_line)
- if stripped_of_quotes is not None:
- self._standard_extraction(self.remaining_item_map[key], stripped_of_quotes)
- self.remaining_item_map.pop(key)
- else:
- self._apply_special_handling(key, line)
-
- @staticmethod
- def _handle_backslash_continuation(line, content_lines) -> str:
- while line.strip().endswith("\\"):
- line = line.strip()[:-1]
- concat_line = content_lines.readline()
- line += concat_line.strip()
- return line
-
- @staticmethod
- def _handle_triple_quoted_string(line, content_lines) -> Tuple[str, bool]:
- result = line
- was_triple_quoted = False
- if '"""' in result:
- was_triple_quoted = True
- while True:
- new_line = content_lines.readline()
- if not new_line:
- raise SyntaxError("Syntax error while reading macro")
- if '"""' in new_line:
- last_line, _, _ = new_line.partition('"""')
- result += last_line + '"""'
- break
- result += new_line
- return result, was_triple_quoted
-
- @staticmethod
- def _strip_quotes(line) -> str:
- line = line.strip()
- stripped_of_quotes = None
- if line.startswith('"""') and line.endswith('"""'):
- stripped_of_quotes = line[3:-3]
- elif (line[0] == '"' and line[-1] == '"') or (line[0] == "'" and line[-1] == "'"):
- stripped_of_quotes = line[1:-1]
- return stripped_of_quotes
-
- def _standard_extraction(self, value: str, match_group: str):
- """For most macro metadata values, this extracts the required data"""
- if isinstance(self.parse_results[value], str):
- self.parse_results[value] = match_group
- if value == "comment":
- self._cleanup_comment()
- elif value == "license":
- self._cleanup_license()
- elif isinstance(self.parse_results[value], list):
- self.parse_results[value] = [of.strip() for of in match_group.split(",")]
- else:
- raise SyntaxError(f"Conflicting data type for {value}")
-
- def _cleanup_comment(self):
- """Remove HTML from the comment line, and truncate it at 512 characters."""
-
- self.parse_results["comment"] = re.sub(r"<.*?>", "", self.parse_results["comment"])
- if len(self.parse_results["comment"]) > 512:
- self.parse_results["comment"] = self.parse_results["comment"][:511] + "…"
-
- def _cleanup_license(self):
- if get_license_manager is not None:
- lm = get_license_manager()
- self.parse_results["license"] = lm.normalize(self.parse_results["license"])
-
- def _apply_special_handling(self, key: str, line: str):
- # Macro authors are supposed to be providing strings here, but in some
- # cases they are not doing so. If this is the "__version__" tag, try
- # to apply some special handling to accept numbers, and "__date__"
- if key == "__version__":
- self._process_noncompliant_version(line)
- self.remaining_item_map.pop(key)
- return
-
- raise SyntaxError(f"Failed to process {key} from {line}")
-
- def _process_noncompliant_version(self, after_equals):
- if is_float(after_equals):
- self.parse_results["version"] = str(after_equals).strip()
- elif "__date__" in after_equals.lower() and self.parse_results["date"]:
- self.parse_results["version"] = self.parse_results["date"]
- else:
- self.parse_results["version"] = "(Unknown)"
- raise SyntaxError(f"Unrecognized version string {after_equals}")
-
- @staticmethod
- def _detect_illegal_content(line: str):
- """Raise a syntax error if this line contains something we can't handle"""
-
- lower_line = line.strip().lower()
- if lower_line.startswith("'") and lower_line.endswith("'"):
- return
- if lower_line.startswith('"') and lower_line.endswith('"'):
- return
- if is_float(lower_line):
- return
- if lower_line == "__date__":
- return
- raise SyntaxError(f"Metadata is expected to be a static string, but got {line}")
-
-
-# Borrowed from Stack Overflow:
-# https://stackoverflow.com/questions/736043/checking-if-a-string-can-be-converted-to-float
-def is_float(element: Any) -> bool:
- """Determine whether a given item can be converted to a floating-point number"""
- try:
- float(element)
- return True
- except ValueError:
- return False
diff --git a/src/Mod/AddonManager/addonmanager_metadata.py b/src/Mod/AddonManager/addonmanager_metadata.py
deleted file mode 100644
index d72f2b92c1..0000000000
--- a/src/Mod/AddonManager/addonmanager_metadata.py
+++ /dev/null
@@ -1,435 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2023 FreeCAD Project Association *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""Classes for working with Addon metadata, as documented at
-https://wiki.FreeCAD.org/Package_metadata"""
-
-from __future__ import annotations
-
-from dataclasses import dataclass, field
-from enum import IntEnum, auto
-from typing import Tuple, Dict, List, Optional
-
-from addonmanager_licenses import get_license_manager
-import addonmanager_freecad_interface as fci
-
-try:
- # If this system provides a secure parser, use that:
- import defusedxml.ElementTree as ET
-except ImportError:
- # Otherwise fall back to the Python standard parser
- import xml.etree.ElementTree as ET
-
-
-@dataclass
-class Contact:
- name: str
- email: str = ""
-
-
-@dataclass
-class License:
- name: str
- file: str = ""
-
-
-class UrlType(IntEnum):
- bugtracker = 0
- discussion = auto()
- documentation = auto()
- readme = auto()
- repository = auto()
- website = auto()
-
- def __str__(self):
- return f"{self.name}"
-
-
-@dataclass
-class Url:
- location: str
- type: UrlType
- branch: str = ""
-
-
-class Version:
- """Provide a more useful representation of Version information"""
-
- def __init__(self, from_string: str = None, from_list=None):
- """If from_string is a string, it is parsed to get the version. If from_list
- exists (and no string was provided), it is treated as a version list of
- [major:int, minor:int, patch:int, pre:str]"""
- self.version_as_list = [0, 0, 0, ""]
- if from_string is not None:
- self._init_from_string(from_string)
- elif from_list is not None:
- self._init_from_list(from_list)
-
- def _init_from_string(self, from_string: str):
- """Find the first digit in the given string, and send that substring off for
- parsing."""
- counter = 0
- for char in from_string:
- if char.isdigit():
- break
- counter += 1
- self._parse_string_to_tuple(from_string[counter:])
-
- def _init_from_list(self, from_list):
- for index, element in enumerate(from_list):
- if index < 3:
- self.version_as_list[index] = int(element)
- elif index == 3:
- self.version_as_list[index] = str(element)
- else:
- break
-
- def _parse_string_to_tuple(self, from_string: str):
- """We hand-parse only simple version strings, of the form 1.2.3suffix -- only
- the first digit is required."""
- splitter = from_string.split(".", 2)
- counter = 0
- for component in splitter:
- try:
- self.version_as_list[counter] = int(component)
- counter += 1
- except ValueError:
- if counter == 0:
- raise ValueError(f"Invalid version string {from_string}")
- number, text = self._parse_final_entry(component)
- self.version_as_list[counter] = number
- self.version_as_list[3] = text
-
- @staticmethod
- def _parse_final_entry(final_string: str) -> Tuple[int, str]:
- """The last value is permitted to contain both a number and a word, and needs
- to be split"""
- digits = ""
- for c in final_string:
- if c.isdigit():
- digits += c
- else:
- break
- return int(digits), final_string[len(digits) :]
-
- def __repr__(self) -> str:
- v = self.version_as_list
- return f"{v[0]}.{v[1]}.{v[2]} {v[3]}"
-
- def __eq__(self, other) -> bool:
- return self.version_as_list == other.version_as_list
-
- def __ne__(self, other) -> bool:
- return not (self == other)
-
- def __lt__(self, other) -> bool:
- for a, b in zip(self.version_as_list, other.version_as_list):
- if a != b:
- return a < b
- return False
-
- def __gt__(self, other) -> bool:
- if self.version_as_list == other.version_as_list:
- return False
- return not (self < other)
-
- def __ge__(self, other) -> bool:
- return self > other or self == other
-
- def __le__(self, other) -> bool:
- return self < other or self == other
-
-
-class DependencyType(IntEnum):
- automatic = 0
- internal = auto()
- addon = auto()
- python = auto()
-
- def __str__(self):
- return f"{self.name}"
-
-
-@dataclass
-class Dependency:
- package: str
- version_lt: str = ""
- version_lte: str = ""
- version_eq: str = ""
- version_gte: str = ""
- version_gt: str = ""
- condition: str = ""
- optional: bool = False
- dependency_type: DependencyType = DependencyType.automatic
-
-
-@dataclass
-class GenericMetadata:
- """Used to store unrecognized elements"""
-
- contents: str = ""
- attributes: Dict[str, str] = field(default_factory=dict)
-
-
-@dataclass
-class Metadata:
- """A pure-python implementation of the Addon Manager's Metadata handling class"""
-
- name: str = ""
- version: Version = None
- date: str = ""
- description: str = ""
- type: str = ""
- maintainer: List[Contact] = field(default_factory=list)
- license: List[License] = field(default_factory=list)
- url: List[Url] = field(default_factory=list)
- author: List[Contact] = field(default_factory=list)
- depend: List[Dependency] = field(default_factory=list)
- conflict: List[Dependency] = field(default_factory=list)
- replace: List[Dependency] = field(default_factory=list)
- tag: List[str] = field(default_factory=list)
- icon: str = ""
- classname: str = ""
- subdirectory: str = ""
- file: List[str] = field(default_factory=list)
- freecadmin: Version = None
- freecadmax: Version = None
- pythonmin: Version = None
- content: Dict[str, List[Metadata]] = field(default_factory=dict) # Recursive def.
-
-
-def get_first_supported_freecad_version(metadata: Metadata) -> Optional[Version]:
- """Look through all content items of this metadata element and determine what the
- first version of freecad that ANY of the items support is. For example, if it
- contains several workbenches, some of which require v0.20, and some 0.21, then
- 0.20 is returned. Returns None if frecadmin is unset by any part of this object."""
-
- current_earliest = metadata.freecadmin if metadata.freecadmin is not None else None
- for content_class in metadata.content.values():
- for content_item in content_class:
- content_first = get_first_supported_freecad_version(content_item)
- if content_first is not None:
- if current_earliest is None:
- current_earliest = content_first
- else:
- current_earliest = min(current_earliest, content_first)
-
- return current_earliest
-
-
-def get_branch_from_metadata(metadata: Metadata) -> str:
- for url in metadata.url:
- if url.type == UrlType.repository:
- return url.branch
- return "master" # Legacy default
-
-
-def get_repo_url_from_metadata(metadata: Metadata) -> str:
- for url in metadata.url:
- if url.type == UrlType.repository:
- return url.location
-
-
-class MetadataReader:
- """Read metadata XML data and construct a Metadata object"""
-
- @staticmethod
- def from_file(filename: str) -> Metadata:
- """A convenience function for loading the Metadata from a file"""
- with open(filename, "rb") as f:
- data = f.read()
- return MetadataReader.from_bytes(data)
-
- @staticmethod
- def from_bytes(data: bytes) -> Metadata:
- """Read XML data from bytes and use it to construct Metadata"""
- element_tree = ET.fromstring(data)
- return MetadataReader._process_element_tree(element_tree)
-
- @staticmethod
- def _process_element_tree(root: ET.Element) -> Metadata:
- """Parse an element tree and convert it into a Metadata object"""
- namespace = MetadataReader._determine_namespace(root)
- return MetadataReader._create_node(namespace, root)
-
- @staticmethod
- def _determine_namespace(root: ET.Element) -> str:
- accepted_namespaces = ["{https://wiki.freecad.org/Package_Metadata}", ""]
- for ns in accepted_namespaces:
- if root.tag == f"{ns}package":
- return ns
- raise RuntimeError("No 'package' element found in metadata file")
-
- @staticmethod
- def _parse_child_element(namespace: str, child: ET.Element, metadata: Metadata):
- """Figure out what sort of metadata child represents, and add it to the
- metadata object."""
-
- tag = child.tag[len(namespace) :]
- if tag in ["name", "date", "description", "type", "icon", "classname", "subdirectory"]:
- # Text-only elements
- metadata.__dict__[tag] = child.text
- elif tag in ["version", "freecadmin", "freecadmax", "pythonmin"]:
- try:
- metadata.__dict__[tag] = Version(from_string=child.text)
- except ValueError:
- print(
- f"Invalid version specified for tag {tag} in Addon {metadata.name}: {child.text}"
- )
- metadata.__dict__[tag] = Version(from_list=[0, 0, 0])
- elif tag in ["tag", "file"]:
- # Lists of strings
- if child.text:
- metadata.__dict__[tag].append(child.text)
- elif tag in ["maintainer", "author"]:
- # Lists of contacts
- metadata.__dict__[tag].append(MetadataReader._parse_contact(child))
- elif tag == "license":
- # List of licenses
- metadata.license.append(MetadataReader._parse_license(child))
- elif tag == "url":
- # List of urls
- metadata.url.append(MetadataReader._parse_url(child))
- elif tag in ["depend", "conflict", "replace"]:
- # Lists of dependencies
- metadata.__dict__[tag].append(MetadataReader._parse_dependency(child))
- elif tag == "content":
- MetadataReader._parse_content(namespace, metadata, child)
-
- @staticmethod
- def _parse_contact(child: ET.Element) -> Contact:
- email = child.attrib["email"] if "email" in child.attrib else ""
- return Contact(name=child.text, email=email)
-
- @staticmethod
- def _parse_license(child: ET.Element) -> License:
- file = child.attrib["file"] if "file" in child.attrib else ""
- license_id = child.text
- lm = get_license_manager()
- license_id = lm.normalize(license_id)
- return License(name=license_id, file=file)
-
- @staticmethod
- def _parse_url(child: ET.Element) -> Url:
- url_type = UrlType.website
- branch = ""
- if "type" in child.attrib and child.attrib["type"] in UrlType.__dict__:
- url_type = UrlType[child.attrib["type"]]
- if url_type == UrlType.repository:
- branch = child.attrib["branch"] if "branch" in child.attrib else ""
- return Url(location=child.text, type=url_type, branch=branch)
-
- @staticmethod
- def _parse_dependency(child: ET.Element) -> Dependency:
- v_lt = child.attrib["version_lt"] if "version_lt" in child.attrib else ""
- v_lte = child.attrib["version_lte"] if "version_lte" in child.attrib else ""
- v_eq = child.attrib["version_eq"] if "version_eq" in child.attrib else ""
- v_gte = child.attrib["version_gte"] if "version_gte" in child.attrib else ""
- v_gt = child.attrib["version_gt"] if "version_gt" in child.attrib else ""
- condition = child.attrib["condition"] if "condition" in child.attrib else ""
- optional = "optional" in child.attrib and child.attrib["optional"].lower() == "true"
- dependency_type = DependencyType.automatic
- if "type" in child.attrib and child.attrib["type"] in DependencyType.__dict__:
- dependency_type = DependencyType[child.attrib["type"]]
- return Dependency(
- child.text,
- version_lt=v_lt,
- version_lte=v_lte,
- version_eq=v_eq,
- version_gte=v_gte,
- version_gt=v_gt,
- condition=condition,
- optional=optional,
- dependency_type=dependency_type,
- )
-
- @staticmethod
- 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", "bundle", "other"]
- for child in root:
- content_type = child.tag[len(namespace) :]
- if content_type in known_content_types:
- if content_type not in metadata.content:
- metadata.content[content_type] = []
- metadata.content[content_type].append(MetadataReader._create_node(namespace, child))
-
- @staticmethod
- def _create_node(namespace, child) -> Metadata:
- new_content_item = Metadata()
- for content_child in child:
- MetadataReader._parse_child_element(namespace, content_child, new_content_item)
- return new_content_item
-
-
-class MetadataWriter:
- """Utility class for serializing a Metadata object into the package.xml standard
- XML file."""
-
- @staticmethod
- def write(metadata: Metadata, path: str):
- """Write the metadata to a file located at path. Overwrites the file if it
- exists. Raises OSError if writing fails."""
- tree = MetadataWriter._create_tree_from_metadata(metadata)
- tree.write(path)
-
- @staticmethod
- def _create_tree_from_metadata(metadata: Metadata) -> ET.ElementTree:
- """Create the XML ElementTree representation of the given Metadata object."""
- tree = ET.ElementTree()
- root = tree.getroot()
- root.attrib["xmlns"] = "https://wiki.freecad.org/Package_Metadata"
- for key, value in metadata.__dict__.items():
- if isinstance(value, str):
- node = ET.SubElement(root, key)
- node.text = value
- else:
- MetadataWriter._create_list_node(metadata, key, root)
- return tree
-
- @staticmethod
- def _create_list_node(metadata: Metadata, key: str, root: ET.Element):
- for item in metadata.__dict__[key]:
- node = ET.SubElement(root, key)
- if key in ["maintainer", "author"]:
- if item.email:
- node.attrib["email"] = item.email
- node.text = item.name
- elif key == "license":
- if item.file:
- node.attrib["file"] = item.file
- node.text = item.name
- elif key == "url":
- if item.branch:
- node.attrib["branch"] = item.branch
- node.attrib["type"] = str(item.type)
- node.text = item.location
- elif key in ["depend", "conflict", "replace"]:
- for dep_key, dep_value in item.__dict__.items():
- if isinstance(dep_value, str) and dep_value:
- node.attrib[dep_key] = dep_value
- elif isinstance(dep_value, bool):
- node.attrib[dep_key] = "True" if dep_value else "False"
- elif isinstance(dep_value, DependencyType):
- node.attrib[dep_key] = str(dep_value)
diff --git a/src/Mod/AddonManager/addonmanager_package_details_controller.py b/src/Mod/AddonManager/addonmanager_package_details_controller.py
deleted file mode 100644
index ccd97a2e7f..0000000000
--- a/src/Mod/AddonManager/addonmanager_package_details_controller.py
+++ /dev/null
@@ -1,284 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2024 The FreeCAD Project Association AISBL *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""Provides the PackageDetails widget."""
-
-import os
-from typing import Optional
-
-from PySide import QtCore, QtWidgets
-
-import addonmanager_freecad_interface as fci
-
-import addonmanager_utilities as utils
-from addonmanager_metadata import (
- Version,
- get_first_supported_freecad_version,
- get_branch_from_metadata,
- get_repo_url_from_metadata,
-)
-from addonmanager_workers_startup import GetMacroDetailsWorker, CheckSingleUpdateWorker
-from addonmanager_git import GitManager, NoGitFound
-from Addon import Addon
-from change_branch import ChangeBranchDialog
-from addonmanager_readme_controller import ReadmeController
-from Widgets.addonmanager_widget_package_details_view import UpdateInformation, WarningFlags
-
-translate = fci.translate
-
-
-class PackageDetailsController(QtCore.QObject):
- """Manages the display of the package README information."""
-
- back = QtCore.Signal()
- install = QtCore.Signal(Addon)
- uninstall = QtCore.Signal(Addon)
- update = QtCore.Signal(Addon)
- execute = QtCore.Signal(Addon)
- update_status = QtCore.Signal(Addon)
-
- def __init__(self, widget=None):
- super().__init__()
- self.ui = widget
- self.readme_controller = ReadmeController(self.ui.readme_browser)
- self.worker = None
- self.addon = None
- self.update_check_thread = None
- self.original_disabled_state = None
- self.original_status = None
- self.check_for_update_worker = None
- try:
- self.git_manager = GitManager()
- except NoGitFound:
- self.git_manager = None
-
- self.ui.button_bar.back.clicked.connect(self.back.emit)
- self.ui.button_bar.run_macro.clicked.connect(lambda: self.execute.emit(self.addon))
- self.ui.button_bar.install.clicked.connect(lambda: self.install.emit(self.addon))
- self.ui.button_bar.uninstall.clicked.connect(lambda: self.uninstall.emit(self.addon))
- self.ui.button_bar.update.clicked.connect(lambda: self.update.emit(self.addon))
- self.ui.button_bar.change_branch.clicked.connect(self.change_branch_clicked)
- self.ui.button_bar.enable.clicked.connect(self.enable_clicked)
- self.ui.button_bar.disable.clicked.connect(self.disable_clicked)
-
- def show_repo(self, repo: Addon) -> None:
- """The main entry point for this class, shows the package details and related buttons
- for the provided repo."""
- self.addon = repo
- self.readme_controller.set_addon(repo)
- self.original_disabled_state = self.addon.is_disabled()
- if repo is not None:
- self.ui.button_bar.show()
- else:
- self.ui.button_bar.hide()
-
- if self.worker is not None:
- if not self.worker.isFinished():
- self.worker.requestInterruption()
- self.worker.wait()
-
- installed = self.addon.status() != Addon.Status.NOT_INSTALLED
- self.ui.set_installed(installed)
- if repo.metadata is not None:
- self.ui.set_url(get_repo_url_from_metadata(repo.metadata))
- else:
- self.ui.set_url(None) # to reset it and hide it
- update_info = UpdateInformation()
- if installed:
- update_info.unchecked = self.addon.status() == Addon.Status.UNCHECKED
- update_info.update_available = self.addon.status() == Addon.Status.UPDATE_AVAILABLE
- update_info.check_in_progress = False # TODO: Implement the "check in progress" status
- if repo.metadata:
- update_info.branch = get_branch_from_metadata(repo.metadata)
- update_info.version = repo.metadata.version
- elif repo.macro:
- update_info.version = repo.macro.version
- self.ui.set_update_available(update_info)
- self.ui.set_location(
- self.addon.macro_directory
- if repo.repo_type == Addon.Kind.MACRO
- else os.path.join(self.addon.mod_directory, self.addon.name)
- )
- self.ui.set_disabled(self.addon.is_disabled())
- self.ui.allow_running(repo.repo_type == Addon.Kind.MACRO)
- self.ui.allow_disabling(repo.repo_type != Addon.Kind.MACRO)
-
- if repo.repo_type == Addon.Kind.MACRO:
- self.update_macro_info(repo)
-
- if repo.status() == Addon.Status.UNCHECKED:
- self.ui.button_bar.check_for_update.show()
- self.ui.button_bar.check_for_update.setText(
- translate("AddonsInstaller", "Check for update")
- )
- self.ui.button_bar.check_for_update.setEnabled(True)
- if not self.update_check_thread:
- self.update_check_thread = QtCore.QThread()
- self.check_for_update_worker = CheckSingleUpdateWorker(repo)
- self.check_for_update_worker.moveToThread(self.update_check_thread)
- self.update_check_thread.finished.connect(self.check_for_update_worker.deleteLater)
- self.ui.button_bar.check_for_update.clicked.connect(
- self.check_for_update_worker.do_work
- )
- self.check_for_update_worker.update_status.connect(self.display_repo_status)
- self.update_check_thread.start()
- else:
- self.ui.button_bar.check_for_update.hide()
-
- flags = WarningFlags()
- flags.required_freecad_version = self.requires_newer_freecad()
- flags.obsolete = repo.obsolete
- flags.python2 = repo.python2
- self.ui.set_warning_flags(flags)
- self.set_change_branch_button_state()
-
- def requires_newer_freecad(self) -> Optional[Version]:
- """If the current package is not installed, returns the first supported version of
- FreeCAD, if one is set, or None if no information is available (or if the package is
- already installed)."""
-
- # If it's not installed, check to see if it's for a newer version of FreeCAD
- if self.addon.status() == Addon.Status.NOT_INSTALLED and self.addon.metadata:
- # Only hide if ALL content items require a newer version, otherwise
- # it's possible that this package actually provides versions of itself
- # for newer and older versions
-
- first_supported_version = get_first_supported_freecad_version(self.addon.metadata)
- if first_supported_version is not None:
- fc_version = Version(from_list=fci.Version())
- if first_supported_version > fc_version:
- return first_supported_version
- return None
-
- def set_change_branch_button_state(self):
- """The change branch button is only available for installed Addons that have a .git directory
- and in runs where the git is available."""
-
- self.ui.button_bar.change_branch.hide()
-
- pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons")
- show_switcher = pref.GetBool("ShowBranchSwitcher", False)
- if not show_switcher:
- return
-
- # Is this repo installed? If not, return.
- if self.addon.status() == Addon.Status.NOT_INSTALLED:
- return
-
- # Is it a Macro? If so, return:
- if self.addon.repo_type == Addon.Kind.MACRO:
- return
-
- # Can we actually switch branches? If not, return.
- if not self.git_manager:
- return
-
- # Is there a .git subdirectory? If not, return.
- basedir = fci.getUserAppDataDir()
- path_to_git = os.path.join(basedir, "Mod", self.addon.name, ".git")
- if not os.path.isdir(path_to_git):
- return
-
- # If all four above checks passed, then it's possible for us to switch
- # branches, if there are any besides the one we are on: show the button
- self.ui.button_bar.change_branch.show()
-
- def update_macro_info(self, repo: Addon) -> None:
- if not repo.macro.url:
- # We need to populate the macro information... may as well do it while the user reads
- # the wiki page
- self.worker = GetMacroDetailsWorker(repo)
- self.worker.readme_updated.connect(self.macro_readme_updated)
- self.worker.start()
-
- def change_branch_clicked(self) -> None:
- """Loads the branch-switching dialog"""
- basedir = fci.getUserAppDataDir()
- path_to_repo = os.path.join(basedir, "Mod", self.addon.name)
- change_branch_dialog = ChangeBranchDialog(path_to_repo, self.ui)
- change_branch_dialog.branch_changed.connect(self.branch_changed)
- change_branch_dialog.exec()
-
- def enable_clicked(self) -> None:
- """Called by the Enable button, enables this Addon and updates GUI to reflect
- that status."""
- self.addon.enable()
- self.ui.set_disabled(False)
- if self.original_disabled_state:
- self.ui.set_new_disabled_status(False)
- self.original_status = self.addon.status()
- self.addon.set_status(Addon.Status.PENDING_RESTART)
- else:
- self.addon.set_status(self.original_status)
- self.update_status.emit(self.addon)
-
- def disable_clicked(self) -> None:
- """Called by the Disable button, disables this Addon and updates the GUI to
- reflect that status."""
- self.addon.disable()
- self.ui.set_disabled(True)
- if not self.original_disabled_state:
- self.ui.set_new_disabled_status(True)
- self.original_status = self.addon.status()
- self.addon.set_status(Addon.Status.PENDING_RESTART)
- else:
- self.addon.set_status(self.original_status)
- self.update_status.emit(self.addon)
-
- def branch_changed(self, old_branch: str, name: str) -> None:
- """Displays a dialog confirming the branch changed, and tries to access the
- metadata file from that branch."""
- # See if this branch has a package.xml file:
- basedir = fci.getUserAppDataDir()
- path_to_metadata = os.path.join(basedir, "Mod", self.addon.name, "package.xml")
- if os.path.isfile(path_to_metadata):
- self.addon.load_metadata_file(path_to_metadata)
- self.addon.installed_version = self.addon.metadata.version
- else:
- self.addon.repo_type = Addon.Kind.WORKBENCH
- self.addon.metadata = None
- self.addon.installed_version = None
- self.addon.updated_timestamp = QtCore.QDateTime.currentDateTime().toSecsSinceEpoch()
- self.addon.branch = name
- self.addon.set_status(Addon.Status.PENDING_RESTART)
- self.ui.set_new_branch(name)
- self.update_status.emit(self.addon)
- QtWidgets.QMessageBox.information(
- self.ui,
- translate("AddonsInstaller", "Success"),
- translate(
- "AddonsInstaller",
- "Branch change succeeded.\n"
- "Moved\n"
- "from: {}\n"
- "to: {}\n"
- "Please restart to use the new version.",
- ).format(old_branch, name),
- )
-
- def display_repo_status(self, addon):
- self.update_status.emit(self.addon)
- self.show_repo(self.addon)
-
- def macro_readme_updated(self):
- self.show_repo(self.addon)
diff --git a/src/Mod/AddonManager/addonmanager_preferences_defaults.json b/src/Mod/AddonManager/addonmanager_preferences_defaults.json
deleted file mode 100644
index ca449421f7..0000000000
--- a/src/Mod/AddonManager/addonmanager_preferences_defaults.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "AddonFlagsURL":
- "https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/addonflags.json",
- "AddonsRemoteCacheURL": "https://addons.freecad.org/metadata.zip",
- "AddonsStatsURL": "https://freecad.org/addon_stats.json",
- "AddonsCacheURL": "https://freecad.org/addons/addon_cache.json",
- "AddonsScoreURL": "NONE",
- "AutoCheck": false,
- "BlockedMacros": "BOLTS,WorkFeatures,how to install,documentation,PartsLibrary,FCGear",
- "CompositeSplitterState": "",
- "CustomRepoHash": "",
- "CustomRepositories": "",
- "CustomToolbarName": "Auto-Created Macro Toolbar",
- "DaysBetweenUpdates": -1,
- "DownloadMacros": false,
- "GitExecutable": "Not set",
- "HideNewerFreeCADRequired": true,
- "HideObsolete": true,
- "HidePy2": true,
- "HideNonOSIApproved": false,
- "HideNonFSFFreeLibre": false,
- "HideUnlicensed": false,
- "KnownPythonVersions": "[]",
- "LastCacheUpdate": "never",
- "MacroCacheUpdateFrequency": 7,
- "MacroGitURL": "https://github.com/FreeCAD/FreeCAD-Macros",
- "MacroUpdateStatsURL": "https://addons.freecad.org/macro_update_stats.json",
- "MacroWikiURL": "https://wiki.freecad.org/Macros_recipes",
- "NoProxyCheck": true,
- "PackageTypeSelection": 0,
- "PrimaryAddonsSubmoduleURL":
- "https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/.gitmodules",
- "ProxyUrl": "",
- "RemoteIconCacheURL": "https://addons.freecad.org/icon_cache.zip",
- "SelectedAddon": "",
- "ShowBranchSwitcher": false,
- "StatusSelection": 0,
- "SystemProxyCheck": false,
- "UpdateFrequencyComboEntry": 0,
- "UserProxyCheck": false,
- "ViewStyle": 1,
- "WindowHeight": 600,
- "WindowWidth": 800,
- "alwaysAskForToolbar": true,
- "devModeLastSelectedLicense": "LGPLv2.1",
- "developerMode": false,
- "disableGit": false,
- "dontShowAddMacroButtonDialog": false,
- "readWarning2022": false
-}
diff --git a/src/Mod/AddonManager/addonmanager_pyside_interface.py b/src/Mod/AddonManager/addonmanager_pyside_interface.py
deleted file mode 100644
index 74629e9c2e..0000000000
--- a/src/Mod/AddonManager/addonmanager_pyside_interface.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022 FreeCAD Project Association *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""Wrap QtCore imports so that can be replaced when running outside of FreeCAD (e.g. for
-unit tests, etc.) Only provides wrappers for the things commonly used by the Addon
-Manager."""
-
-try:
- from PySide import QtCore
-
- QObject = QtCore.QObject
- Signal = QtCore.Signal
-
- def is_interruption_requested() -> bool:
- return QtCore.QThread.currentThread().isInterruptionRequested()
-
-except ImportError:
- QObject = object
-
- class Signal:
- """A purely synchronous signal. emit() does not use queued slots so cannot be
- used across threads."""
-
- def __init__(self, *args):
- self.expected_types = args
- self.connections = []
-
- def connect(self, func):
- self.connections.append(func)
-
- def disconnect(self, func):
- if func in self.connections:
- self.connections.remove(func)
-
- def emit(self, *args):
- for connection in self.connections:
- connection(args)
-
- def is_interruption_requested() -> bool:
- return False
diff --git a/src/Mod/AddonManager/addonmanager_python_deps_gui.py b/src/Mod/AddonManager/addonmanager_python_deps_gui.py
deleted file mode 100644
index 8123ef9930..0000000000
--- a/src/Mod/AddonManager/addonmanager_python_deps_gui.py
+++ /dev/null
@@ -1,505 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2025 FreeCAD Project Association AISBL *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""Provides classes and support functions for managing the automatically-installed
-Python library dependencies. No support is provided for uninstalling those dependencies
-because pip's uninstall function does not support the target directory argument."""
-
-import json
-import os
-import platform
-import shutil
-import subprocess
-import sys
-from functools import partial
-from typing import Dict, Iterable, List, Tuple, TypedDict
-from addonmanager_utilities import create_pip_call
-
-import addonmanager_freecad_interface as fci
-
-try:
- from PySide import QtCore, QtGui, QtWidgets
-except ImportError:
- try:
- from PySide6 import QtCore, QtGui, QtWidgets
- except ImportError:
- from PySide2 import QtCore, QtGui, QtWidgets
-
-# Make sure this can run inside and outside FreeCAD, and don't require that (when run inside FreeCAD) the user has the
-# python QtUiTools installed, because FreeCAD wraps it for us.
-try:
- import FreeCADGui
-
- loadUi = FreeCADGui.PySideUic.loadUi
-except ImportError:
- try:
- from PySide6.QtUiTools import QUiLoader
- except ImportError:
- from PySide2.QtUiTools import QUiLoader
-
- def loadUi(ui_file: str) -> QtWidgets.QWidget:
- q_ui_file = QtCore.QFile(ui_file)
- q_ui_file.open(QtCore.QFile.OpenModeFlag.ReadOnly)
- loader = QUiLoader()
- return loader.load(ui_file)
-
-
-try:
- from freecad.utils import get_python_exe
-except ImportError:
-
- def get_python_exe():
- return shutil.which("python")
-
-
-import addonmanager_utilities as utils
-
-translate = fci.translate
-
-
-# pylint: disable=too-few-public-methods
-
-
-class PipFailed(Exception):
- """Exception thrown when pip times out or otherwise fails to return valid results"""
-
-
-class CheckForPythonPackageUpdatesWorker(QtCore.QThread):
- """Perform non-blocking Python library update availability checking"""
-
- python_package_updates_available = QtCore.Signal()
-
- def __init__(self):
- QtCore.QThread.__init__(self)
-
- def run(self):
- """Usually not called directly: instead, instantiate this class and call its start()
- function in a parent thread. emits a python_package_updates_available signal if updates
- are available for any of the installed Python packages."""
-
- if python_package_updates_are_available():
- self.python_package_updates_available.emit()
-
-
-def python_package_updates_are_available() -> bool:
- """Returns True if any of the Python packages installed into the AdditionalPythonPackages
- directory have updates available, or False if they are all up-to-date."""
-
- vendor_path = os.path.join(fci.DataPaths().data_dir, "AdditionalPythonPackages")
- package_counter = 0
- try:
- outdated_packages_stdout = call_pip(["list", "-o", "--path", vendor_path])
- except PipFailed as e:
- fci.Console.PrintError(str(e) + "\n")
- return False
- fci.Console.PrintLog("Output from pip -o:\n")
- for line in outdated_packages_stdout:
- if len(line) > 0:
- package_counter += 1
- fci.Console.PrintLog(f" {line}\n")
- return package_counter > 0
-
-
-def call_pip(args: List[str]) -> List[str]:
- """Tries to locate the appropriate Python executable and run pip with version checking
- disabled. Fails if Python can't be found or if pip is not installed."""
-
- try:
- call_args = create_pip_call(args)
- except RuntimeError as exception:
- raise PipFailed() from exception
-
- try:
- proc = utils.run_interruptable_subprocess(call_args)
- except subprocess.CalledProcessError as exception:
- raise PipFailed("pip timed out") from exception
-
- if proc.returncode != 0:
- raise PipFailed(proc.stderr)
-
- data = proc.stdout
- return data.split("\n")
-
-
-def parse_pip_list_output(all_packages, outdated_packages) -> Dict[str, Dict[str, str]]:
- """Parses the output from pip into a dictionary with update information in it. The pip
- output should be an array of lines of text."""
-
- # All Packages output looks like this:
- # Package Version
- # ---------- -------
- # gitdb 4.0.9
- # setuptools 41.2.0
-
- # Outdated Packages output looks like this:
- # Package Version Latest Type
- # ---------- ------- ------ -----
- # pip 21.0.1 22.1.2 wheel
- # setuptools 41.2.0 63.2.0 wheel
-
- packages = {}
- skip_counter = 0
- for line in all_packages:
- if skip_counter < 2:
- skip_counter += 1
- continue
- entries = line.split()
- if len(entries) > 1:
- package_name = entries[0]
- installed_version = entries[1]
- packages[package_name] = {
- "installed_version": installed_version,
- "available_version": "",
- }
-
- skip_counter = 0
- for line in outdated_packages:
- if skip_counter < 2:
- skip_counter += 1
- continue
- entries = line.split()
- if len(entries) > 1:
- package_name = entries[0]
- installed_version = entries[1]
- available_version = entries[2]
- packages[package_name] = {
- "installed_version": installed_version,
- "available_version": available_version,
- }
-
- return packages
-
-
-class PythonPackageManager:
- """A GUI-based pip interface allowing packages to be updated, either individually or all at
- once."""
-
- class PipRunner(QtCore.QObject):
- """Run pip in a separate thread so the UI doesn't block while it runs"""
-
- finished = QtCore.Signal()
- error = QtCore.Signal(str)
-
- def __init__(self, vendor_path, parent=None):
- super().__init__(parent)
- self.all_packages_stdout = []
- self.outdated_packages_stdout = []
- self.vendor_path = vendor_path
- self.package_list = {}
-
- def process(self):
- """Execute this object."""
- try:
- self.all_packages_stdout = call_pip(["list", "--path", self.vendor_path])
- self.outdated_packages_stdout = call_pip(["list", "-o", "--path", self.vendor_path])
- except PipFailed as e:
- fci.Console.PrintError(str(e) + "\n")
- self.error.emit(str(e))
- self.finished.emit()
-
- class DependentAddon(TypedDict):
- name: str
- optional: bool
-
- def __init__(self, addons):
- self.dlg = loadUi(
- os.path.join(os.path.dirname(__file__), "PythonDependencyUpdateDialog.ui")
- )
-
- self.addons = addons
- self.vendor_path = utils.get_pip_target_directory()
- self.worker_thread = None
- self.worker_object = None
- self.package_list = []
-
- def show(self):
- """Run the modal dialog"""
-
- known_python_versions = self.get_known_python_versions()
- if self._current_python_version_is_new() and known_python_versions:
- # pylint: disable=line-too-long
- result = QtWidgets.QMessageBox.question(
- None,
- translate("AddonsInstaller", "New Python Version Detected"),
- translate(
- "AddonsInstaller",
- "This appears to be the first time this version of Python has been used with the Addon Manager. "
- "Would you like to install the same auto-installed dependencies for it?",
- ),
- QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
- )
- if result == QtWidgets.QMessageBox.StandardButton.Yes:
- self._reinstall_all_packages()
-
- self._add_current_python_version()
- self._create_list_from_pip()
- self.dlg.tableWidget.setSortingEnabled(False)
- self.dlg.labelInstallationPath.setText(self.vendor_path)
- self.dlg.exec()
-
- def _create_list_from_pip(self):
- """Uses pip and pip -o to generate a list of installed packages, and creates the user
- interface elements for those packages. Asynchronous, will complete AFTER the window is
- showing in most cases."""
-
- self.worker_thread = QtCore.QThread()
- self.worker_object = PythonPackageManager.PipRunner(self.vendor_path)
- self.worker_object.moveToThread(self.worker_thread)
- self.worker_object.finished.connect(self._worker_finished)
- self.worker_object.finished.connect(self.worker_thread.quit)
- self.worker_thread.started.connect(self.worker_object.process)
- self.worker_thread.start()
-
- self.dlg.tableWidget.setRowCount(1)
- self.dlg.tableWidget.setItem(
- 0,
- 0,
- QtWidgets.QTableWidgetItem(translate("AddonsInstaller", "Processing, please wait...")),
- )
- self.dlg.tableWidget.horizontalHeader().setSectionResizeMode(
- 0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents
- )
-
- def _worker_finished(self):
- """Callback for when the worker process has completed"""
- all_packages_stdout = self.worker_object.all_packages_stdout
- outdated_packages_stdout = self.worker_object.outdated_packages_stdout
-
- self.package_list = parse_pip_list_output(all_packages_stdout, outdated_packages_stdout)
- self.dlg.buttonUpdateAll.clicked.connect(
- partial(self._update_all_packages, self.package_list)
- )
-
- self.dlg.tableWidget.setRowCount(len(self.package_list))
- update_buttons = []
- counter = 0
- update_counter = 0
- self.dlg.tableWidget.setSortingEnabled(False)
- for package_name, package_details in self.package_list.items():
- dependent_addons = self._get_dependent_addons(package_name)
- dependencies = []
- for addon in dependent_addons:
- if addon["optional"]:
- dependencies.append(addon["name"] + "*")
- else:
- dependencies.append(addon["name"])
- self.dlg.tableWidget.setItem(counter, 0, QtWidgets.QTableWidgetItem(package_name))
- self.dlg.tableWidget.setItem(
- counter,
- 1,
- QtWidgets.QTableWidgetItem(package_details["installed_version"]),
- )
- self.dlg.tableWidget.setItem(
- counter,
- 2,
- QtWidgets.QTableWidgetItem(package_details["available_version"]),
- )
- self.dlg.tableWidget.setItem(
- counter,
- 3,
- QtWidgets.QTableWidgetItem(", ".join(dependencies)),
- )
- if len(package_details["available_version"]) > 0:
- update_buttons.append(QtWidgets.QPushButton(translate("AddonsInstaller", "Update")))
- update_buttons[-1].setIcon(QtGui.QIcon(":/icons/button_up.svg"))
- update_buttons[-1].clicked.connect(partial(self._update_package, package_name))
- self.dlg.tableWidget.setCellWidget(counter, 4, update_buttons[-1])
- update_counter += 1
- else:
- self.dlg.tableWidget.removeCellWidget(counter, 3)
- counter += 1
- self.dlg.tableWidget.setSortingEnabled(True)
-
- self.dlg.tableWidget.horizontalHeader().setStretchLastSection(False)
- self.dlg.tableWidget.horizontalHeader().setSectionResizeMode(
- 0, QtWidgets.QHeaderView.ResizeMode.Stretch
- )
- self.dlg.tableWidget.horizontalHeader().setSectionResizeMode(
- 1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents
- )
- self.dlg.tableWidget.horizontalHeader().setSectionResizeMode(
- 2, QtWidgets.QHeaderView.ResizeMode.ResizeToContents
- )
- self.dlg.tableWidget.horizontalHeader().setSectionResizeMode(
- 3, QtWidgets.QHeaderView.ResizeMode.ResizeToContents
- )
-
- if update_counter > 0:
- self.dlg.buttonUpdateAll.setEnabled(True)
- else:
- self.dlg.buttonUpdateAll.setEnabled(False)
-
- def _get_dependent_addons(self, package) -> List[DependentAddon]:
- dependent_addons = []
- for addon in self.addons:
- # if addon.installed_version is not None:
- if package.lower() in addon.python_requires:
- dependent_addons.append({"name": addon.name, "optional": False})
- elif package.lower() in addon.python_optional:
- dependent_addons.append({"name": addon.name, "optional": True})
- return dependent_addons
-
- def _update_package(self, package_name) -> None:
- """Run pip --upgrade on the given package. Updates all dependent packages as well."""
- for line in range(self.dlg.tableWidget.rowCount()):
- if self.dlg.tableWidget.item(line, 0).text() == package_name:
- self.dlg.tableWidget.setItem(
- line,
- 2,
- QtWidgets.QTableWidgetItem(translate("AddonsInstaller", "Updating...")),
- )
- break
- QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 50)
-
- try:
- fci.Console.PrintLog(
- f"Running 'pip install --upgrade --target {self.vendor_path} {package_name}'\n"
- )
- call_pip(["install", "--upgrade", package_name, "--target", self.vendor_path])
- self._create_list_from_pip()
- while self.worker_thread.isRunning():
- QtCore.QCoreApplication.processEvents(
- QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 50
- )
- except PipFailed as e:
- fci.Console.PrintError(str(e) + "\n")
- return
- QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 50)
-
- def _update_all_packages(self, package_list) -> None:
- """Updates all packages with available updates."""
- updates = []
- for package_name, package_details in package_list.items():
- if (
- len(package_details["available_version"]) > 0
- and package_details["available_version"] != package_details["installed_version"]
- ):
- updates.append(package_name)
-
- fci.Console.PrintLog(f"Running update for {len(updates)} Python packages...\n")
- for package_name in updates:
- self._update_package(package_name)
-
- @classmethod
- def migrate_old_am_installations(cls) -> bool:
- """Move packages installed before the Addon Manager switched to a versioned directory
- structure into the versioned structure. Returns True if a migration was done, or false
- if no migration was needed."""
-
- migrated = False
-
- old_directory = os.path.join(fci.DataPaths().data_dir, "AdditionalPythonPackages")
-
- new_directory = utils.get_pip_target_directory()
- new_directory_name = new_directory.rsplit(os.path.sep, 1)[1]
-
- if not os.path.exists(old_directory) or os.path.exists(
- os.path.join(old_directory, "MIGRATION_COMPLETE")
- ):
- # Nothing to migrate
- return False
-
- os.makedirs(new_directory, mode=0o777, exist_ok=True)
-
- for content_item in os.listdir(old_directory):
- if content_item == new_directory_name:
- continue
- old_path = os.path.join(old_directory, content_item)
- new_path = os.path.join(new_directory, content_item)
- fci.Console.PrintLog(
- f"Moving {content_item} into the new (versioned) directory structure\n"
- )
- fci.Console.PrintLog(f" {old_path} --> {new_path}\n")
- shutil.move(old_path, new_path)
- migrated = True
-
- sys.path.append(new_directory)
- cls._add_current_python_version()
-
- with open(os.path.join(old_directory, "MIGRATION_COMPLETE"), "w", encoding="utf-8") as f:
- f.write("Files originally installed in this directory have been migrated to:\n")
- f.write(new_directory)
- f.write(
- "\nThe existence of this file prevents the Addon Manager from "
- "attempting the migration again.\n"
- )
- return migrated
-
- @classmethod
- def get_known_python_versions(cls) -> List[Tuple[int, int]]:
- """Get the list of Python versions that the Addon Manager has seen before."""
- known_python_versions_string = fci.Preferences().get("KnownPythonVersions")
- known_python_versions = json.loads(known_python_versions_string)
- return known_python_versions
-
- @classmethod
- def _add_current_python_version(cls) -> None:
- known_python_versions = cls.get_known_python_versions()
- major, minor, _ = platform.python_version_tuple()
- if not [major, minor] in known_python_versions:
- known_python_versions.append((major, minor))
- fci.Preferences().set("KnownPythonVersions", json.dumps(known_python_versions))
-
- @classmethod
- def _current_python_version_is_new(cls) -> bool:
- """Returns True if this is the first time the Addon Manager has seen this version of
- Python"""
- known_python_versions = cls.get_known_python_versions()
- major, minor, _ = platform.python_version_tuple()
- if not [major, minor] in known_python_versions:
- return True
- return False
-
- def _load_old_package_list(self) -> Iterable[str]:
- """Gets iterable of packages from the package installation manifest"""
-
- known_python_versions = self.get_known_python_versions()
- if not known_python_versions:
- return []
- last_version = known_python_versions[-1]
- expected_directory = f"py{last_version[0]}{last_version[1]}"
- expected_directory = os.path.join(
- fci.DataPaths().data_dir, "AdditionalPythonPackages", expected_directory
- )
- # For now just do this synchronously
- worker_object = PythonPackageManager.PipRunner(expected_directory)
- worker_object.process()
- packages = parse_pip_list_output(
- worker_object.all_packages_stdout, worker_object.outdated_packages_stdout
- )
- return packages.keys()
-
- def _reinstall_all_packages(self) -> None:
- """Loads the package manifest from another Python version, and installs the same packages
- for the current (presumably new) version of Python."""
-
- packages = self._load_old_package_list()
- args = ["install"]
- args.extend(packages)
- args.extend(["--target", self.vendor_path])
-
- try:
- call_pip(args)
- except PipFailed as e:
- fci.Console.PrintError(str(e) + "\n")
- return
diff --git a/src/Mod/AddonManager/addonmanager_readme_controller.py b/src/Mod/AddonManager/addonmanager_readme_controller.py
deleted file mode 100644
index d7b1624a9f..0000000000
--- a/src/Mod/AddonManager/addonmanager_readme_controller.py
+++ /dev/null
@@ -1,286 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2024 The FreeCAD Project Association AISBL *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""A Qt Widget for displaying Addon README information"""
-
-import FreeCAD
-from Addon import Addon
-import addonmanager_utilities as utils
-
-from enum import IntEnum, Enum, auto
-from html.parser import HTMLParser
-from typing import Optional
-
-import NetworkManager
-
-translate = FreeCAD.Qt.translate
-
-from PySide import QtCore, QtGui
-
-
-class ReadmeDataType(IntEnum):
- PlainText = 0
- Markdown = 1
- Html = 2
-
-
-class ReadmeController(QtCore.QObject):
- """A class that can provide README data from an Addon, possibly loading external resources such
- as images"""
-
- def __init__(self, widget):
- super().__init__()
- NetworkManager.InitializeNetworkManager()
- NetworkManager.AM_NETWORK_MANAGER.completed.connect(self._download_completed)
- self.readme_request_index = 0
- self.resource_requests = {}
- self.resource_failures = []
- self.url = ""
- self.readme_data = None
- self.readme_data_type = None
- self.addon: Optional[Addon] = None
- self.stop = True
- self.widget = widget
- self.widget.load_resource.connect(self.loadResource)
- self.widget.follow_link.connect(self.follow_link)
-
- def set_addon(self, repo: Addon):
- """Set which Addon's information is displayed"""
-
- self.addon = repo
- self.stop = False
- self.readme_data = None
- if self.addon.repo_type == Addon.Kind.MACRO:
- self.url = self.addon.macro.wiki
- if not self.url:
- self.url = self.addon.macro.url
- if not self.url:
- self.widget.setText(
- translate(
- "AddonsInstaller",
- "Loading info for {} from the FreeCAD Macro Recipes wiki...",
- ).format(self.addon.display_name, self.url)
- )
- return
- else:
- self.url = utils.get_readme_url(repo)
- self.widget.setUrl(self.url)
-
- self.widget.setText(
- translate("AddonsInstaller", "Loading page for {} from {}...").format(
- self.addon.display_name, self.url
- )
- )
- self.readme_request_index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
- self.url
- )
-
- def _download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> None:
- """Callback for handling a completed README file download."""
- if index == self.readme_request_index:
- if code == 200: # HTTP success
- self._process_package_download(data.data().decode("utf-8"))
- else:
- self.widget.setText(
- translate(
- "AddonsInstaller",
- "Failed to download data from {} -- received response code {}.",
- ).format(self.url, code)
- )
- elif index in self.resource_requests:
- if code == 200:
- self._process_resource_download(self.resource_requests[index], data.data())
- else:
- FreeCAD.Console.PrintLog(f"Failed to load {self.resource_requests[index]}\n")
- self.resource_failures.append(self.resource_requests[index])
- del self.resource_requests[index]
- if not self.resource_requests:
- if self.readme_data:
- if self.readme_data_type == ReadmeDataType.Html:
- self.widget.setHtml(self.readme_data)
- elif self.readme_data_type == ReadmeDataType.Markdown:
- self.widget.setMarkdown(self.readme_data)
- else:
- self.widget.setText(self.readme_data)
- else:
- self.set_addon(self.addon) # Trigger a reload of the page now with resources
-
- def _process_package_download(self, data: str):
- if self.addon.repo_type == Addon.Kind.MACRO:
- parser = WikiCleaner()
- parser.feed(data)
- self.readme_data = parser.final_html
- self.readme_data_type = ReadmeDataType.Html
- self.widget.setHtml(parser.final_html)
- else:
- self.readme_data = data
- self.readme_data_type = ReadmeDataType.Markdown
- self.widget.setMarkdown(data)
-
- def _process_resource_download(self, resource_name: str, resource_data: bytes):
- image = QtGui.QImage.fromData(resource_data)
- self.widget.set_resource(resource_name, image)
-
- def loadResource(self, full_url: str):
- if full_url not in self.resource_failures:
- index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(full_url)
- self.resource_requests[index] = full_url
-
- def cancel_resource_loading(self):
- self.stop = True
- for request in self.resource_requests:
- NetworkManager.AM_NETWORK_MANAGER.abort(request)
- self.resource_requests.clear()
-
- def follow_link(self, url: str) -> None:
- final_url = url
- if not url.startswith("http"):
- if url.endswith(".md"):
- final_url = self._create_markdown_url(url)
- else:
- final_url = self._create_full_url(url)
- FreeCAD.Console.PrintLog(f"Loading {final_url} in the system browser")
- QtGui.QDesktopServices.openUrl(final_url)
-
- def _create_full_url(self, url: str) -> str:
- if url.startswith("http"):
- return url
- if not self.url:
- return url
- lhs, slash, _ = self.url.rpartition("/")
- return lhs + slash + url
-
- def _create_markdown_url(self, file: str) -> str:
- base_url = utils.get_readme_html_url(self.addon)
- lhs, slash, _ = base_url.rpartition("/")
- return lhs + slash + file
-
-
-class WikiCleaner(HTMLParser):
- """This HTML parser cleans up FreeCAD Macro Wiki Page for display in a
- QTextBrowser widget (which does not deal will with tables used as formatting,
- etc.) It strips out any tables, and extracts the mw-parser-output div as the only
- thing that actually gets displayed. It also discards anything inside the [edit]
- spans that litter wiki output."""
-
- class State(Enum):
- BeforeMacroContent = auto()
- InMacroContent = auto()
- InTable = auto()
- InEditSpan = auto()
- AfterMacroContent = auto()
-
- def __init__(self):
- super().__init__()
- self.depth_in_div = 0
- self.depth_in_span = 0
- self.depth_in_table = 0
- self.final_html = ""
- self.previous_state = WikiCleaner.State.BeforeMacroContent
- self.state = WikiCleaner.State.BeforeMacroContent
-
- def handle_starttag(self, tag: str, attrs):
- if tag == "div":
- self.handle_div_start(attrs)
- elif tag == "span":
- self.handle_span_start(attrs)
- elif tag == "table":
- self.handle_table_start(attrs)
- else:
- if self.state == WikiCleaner.State.InMacroContent:
- self.add_tag_to_html(tag, attrs)
-
- def handle_div_start(self, attrs):
- for name, value in attrs:
- if name == "class" and value == "mw-parser-output":
- self.previous_state = self.state
- self.state = WikiCleaner.State.InMacroContent
- if self.state == WikiCleaner.State.InMacroContent:
- self.depth_in_div += 1
- self.add_tag_to_html("div", attrs)
-
- def handle_span_start(self, attrs):
- for name, value in attrs:
- if name == "class" and value == "mw-editsection":
- self.previous_state = self.state
- self.state = WikiCleaner.State.InEditSpan
- break
- if self.state == WikiCleaner.State.InEditSpan:
- self.depth_in_span += 1
- elif WikiCleaner.State.InMacroContent:
- self.add_tag_to_html("span", attrs)
-
- def handle_table_start(self, unused):
- if self.state != WikiCleaner.State.InTable:
- self.previous_state = self.state
- self.state = WikiCleaner.State.InTable
- self.depth_in_table += 1
-
- def add_tag_to_html(self, tag, attrs=None):
- self.final_html += f"<{tag}"
- if attrs:
- self.final_html += " "
- for attr, value in attrs:
- self.final_html += f"{attr}='{value}'"
- self.final_html += ">\n"
-
- def handle_endtag(self, tag):
- if tag == "table":
- self.handle_table_end()
- elif tag == "span":
- self.handle_span_end()
- elif tag == "div":
- self.handle_div_end()
- else:
- if self.state == WikiCleaner.State.InMacroContent:
- self.add_tag_to_html(f"/{tag}")
-
- def handle_span_end(self):
- if self.state == WikiCleaner.State.InEditSpan:
- self.depth_in_span -= 1
- if self.depth_in_span <= 0:
- self.depth_in_span = 0
- self.state = self.previous_state
- else:
- self.add_tag_to_html(f"/span")
-
- def handle_div_end(self):
- if self.state == WikiCleaner.State.InMacroContent:
- self.depth_in_div -= 1
- if self.depth_in_div <= 0:
- self.depth_in_div = 0
- self.state = WikiCleaner.State.AfterMacroContent
- self.final_html += ""
- else:
- self.add_tag_to_html(f"/div")
-
- def handle_table_end(self):
- if self.state == WikiCleaner.State.InTable:
- self.depth_in_table -= 1
- if self.depth_in_table <= 0:
- self.depth_in_table = 0
- self.state = self.previous_state
-
- def handle_data(self, data):
- if self.state == WikiCleaner.State.InMacroContent:
- self.final_html += data
diff --git a/src/Mod/AddonManager/addonmanager_uninstaller.py b/src/Mod/AddonManager/addonmanager_uninstaller.py
deleted file mode 100644
index 448031e890..0000000000
--- a/src/Mod/AddonManager/addonmanager_uninstaller.py
+++ /dev/null
@@ -1,292 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022 FreeCAD Project Association *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""Contains the classes to manage Addon removal: intended as a stable API, safe for
-external code to call and to rely upon existing. See classes AddonUninstaller and
-MacroUninstaller for details."""
-import json
-import os
-from typing import List
-
-import addonmanager_freecad_interface as fci
-from addonmanager_pyside_interface import QObject, Signal
-
-import addonmanager_utilities as utils
-from Addon import Addon
-
-translate = fci.translate
-
-# pylint: disable=too-few-public-methods
-
-
-class InvalidAddon(RuntimeError):
- """Raised when an object that cannot be uninstalled is passed to the constructor"""
-
-
-class AddonUninstaller(QObject):
- """The core, non-GUI uninstaller class for non-macro addons. Usually instantiated
- and moved to its own thread, otherwise it will block the GUI (if the GUI is
- running) -- since all it does is delete files this is not a huge problem,
- but in some cases the Addon might be quite large, and deletion may take a
- non-trivial amount of time.
-
- In all cases in this class, the generic Python 'object' argument to the init
- function is intended to be an Addon-like object that provides, at a minimum,
- a 'name' attribute. The Addon manager uses the Addon class for this purpose,
- but external code may use any other class that meets that criterion.
-
- Recommended Usage (when running with the GUI up, so you don't block the GUI thread):
-
- addon_to_remove = MyAddon() # Some class with 'name' attribute
-
- self.worker_thread = QThread()
- self.uninstaller = AddonUninstaller(addon_to_remove)
- self.uninstaller.moveToThread(self.worker_thread)
- self.uninstaller.success.connect(self.removal_succeeded)
- self.uninstaller.failure.connect(self.removal_failed)
- self.uninstaller.finished.connect(self.worker_thread.quit)
- self.worker_thread.started.connect(self.uninstaller.run)
- self.worker_thread.start() # Returns immediately
-
- # On success, the connections above result in self.removal_succeeded being
- emitted, and # on failure, self.removal_failed is emitted.
-
-
- Recommended non-GUI usage (blocks until complete):
-
- addon_to_remove = MyAddon() # Some class with 'name' attribute
- uninstaller = AddonInstaller(addon_to_remove)
- uninstaller.run()
-
- """
-
- # Signals: success and failure Emitted when the installation process is complete.
- # The object emitted is the object that the installation was requested for.
- success = Signal(object)
- failure = Signal(object, str)
-
- # Finished: regardless of the outcome, this is emitted when all work that is
- # going to be done is done (i.e. whatever thread this is running in can quit).
- finished = Signal()
-
- def __init__(self, addon: Addon):
- """Initialize the uninstaller."""
- super().__init__()
- self.addon_to_remove = addon
- self.installation_path = fci.DataPaths().mod_dir
- self.macro_installation_path = fci.DataPaths().macro_dir
-
- def run(self) -> bool:
- """Remove an addon. Returns True if the addon was removed cleanly, or False
- if not. Emits either success or failure prior to returning."""
- success = False
- error_message = translate("AddonsInstaller", "An unknown error occurred")
- if hasattr(self.addon_to_remove, "name") and self.addon_to_remove.name:
- # Make sure we don't accidentally remove the Mod directory
- path_to_remove = os.path.normpath(
- os.path.join(self.installation_path, self.addon_to_remove.name)
- )
- if os.path.exists(path_to_remove) and not os.path.samefile(
- path_to_remove, self.installation_path
- ):
- try:
- self.run_uninstall_script(path_to_remove)
- self.remove_extra_files(path_to_remove)
- success = utils.rmdir(path_to_remove)
- if (
- hasattr(self.addon_to_remove, "contains_workbench")
- and self.addon_to_remove.contains_workbench()
- ):
- self.addon_to_remove.desinstall_workbench()
- except OSError as e:
- error_message = str(e)
- else:
- error_message = translate(
- "AddonsInstaller",
- "Could not find addon {} to remove it.",
- ).format(self.addon_to_remove.name)
- if success:
- self.success.emit(self.addon_to_remove)
- else:
- self.failure.emit(self.addon_to_remove, error_message)
- self.addon_to_remove.set_status(Addon.Status.NOT_INSTALLED)
- self.finished.emit()
- return success
-
- @staticmethod
- def run_uninstall_script(path_to_remove):
- """Run the addon's uninstaller.py script, if it exists"""
- uninstall_script = os.path.join(path_to_remove, "uninstall.py")
- if os.path.exists(uninstall_script):
- # pylint: disable=broad-exception-caught
- try:
- with open(uninstall_script, encoding="utf-8") as f:
- exec(f.read())
- except Exception:
- fci.Console.PrintError(
- translate(
- "AddonsInstaller",
- "Execution of Addon's uninstall.py script failed. Proceeding with uninstall...",
- )
- + "\n"
- )
-
- @staticmethod
- def remove_extra_files(path_to_remove):
- """When installing, an extra file called AM_INSTALLATION_DIGEST.txt may be
- created, listing extra files that the installer put into place. Remove those
- files."""
- digest = os.path.join(path_to_remove, "AM_INSTALLATION_DIGEST.txt")
- if not os.path.exists(digest):
- return
- with open(digest, encoding="utf-8") as f:
- lines = f.readlines()
- for line in lines:
- stripped = line.strip()
- if len(stripped) > 0 and stripped[0] != "#" and os.path.exists(stripped):
- try:
- os.unlink(stripped)
- fci.Console.PrintMessage(
- translate("AddonsInstaller", "Removed extra installed file {}").format(
- stripped
- )
- + "\n"
- )
- except FileNotFoundError:
- pass # Great, no need to remove then!
- except OSError as e:
- # Strange error to receive here, but just continue and print
- # out an error to the console
- fci.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Error while trying to remove extra installed file {}",
- ).format(stripped)
- + "\n"
- )
- fci.Console.PrintWarning(str(e) + "\n")
-
-
-class MacroUninstaller(QObject):
- """The core, non-GUI uninstaller class for macro addons. May be run directly on
- the GUI thread if desired, since macros are intended to be relatively small and
- shouldn't have too many files to delete. However, it is a QObject so may also be
- moved into a QThread -- see AddonUninstaller documentation for details of that
- implementation.
-
- The Python object passed in is expected to provide a "macro" subobject,
- which itself is required to provide at least a "filename" attribute, and may also
- provide an "icon", "xpm", and/or "other_files" attribute. All filenames provided
- by those attributes are expected to be relative to the installed location of the
- "filename" macro file (usually the main FreeCAD user macros directory)."""
-
- # Signals: success and failure Emitted when the removal process is complete. The
- # object emitted is the object that the removal was requested for.
- success = Signal(object)
- failure = Signal(object, str)
-
- # Finished: regardless of the outcome, this is emitted when all work that is
- # going to be done is done (i.e. whatever thread this is running in can quit).
- finished = Signal()
-
- def __init__(self, addon):
- super().__init__()
- self.installation_location = fci.DataPaths().macro_dir
- self.addon_to_remove = addon
- if (
- not hasattr(self.addon_to_remove, "macro")
- or not self.addon_to_remove.macro
- or not hasattr(self.addon_to_remove.macro, "filename")
- or not self.addon_to_remove.macro.filename
- ):
- raise InvalidAddon()
-
- def run(self):
- """Execute the removal process."""
- success = True
- errors = []
- directories = set()
- for f in self._get_files_to_remove():
- normed = os.path.normpath(f)
- if os.path.isabs(normed):
- full_path = normed
- else:
- full_path = os.path.join(self.installation_location, normed)
- if "/" in f:
- directories.add(os.path.dirname(full_path))
- try:
- os.unlink(full_path)
- fci.Console.PrintLog(f"Removed macro file {full_path}\n")
- except FileNotFoundError:
- pass # Great, no need to remove then!
- except OSError as e:
- # Probably permission denied, or something like that
- errors.append(
- translate(
- "AddonsInstaller",
- "Error while trying to remove macro file {}:",
- ).format(full_path)
- + " "
- + str(e)
- )
- success = False
- except Exception:
- # Generic catch-all, just in case (because failure to catch an exception
- # here can break things pretty badly)
- success = False
-
- self._cleanup_directories(directories)
-
- if success:
- self.success.emit(self.addon_to_remove)
- else:
- self.failure.emit(self.addon_to_remove, "\n".join(errors))
- self.addon_to_remove.set_status(Addon.Status.NOT_INSTALLED)
- self.finished.emit()
-
- def _get_files_to_remove(self) -> List[str]:
- """Get the list of files that should be removed"""
- manifest_file = os.path.join(
- self.installation_location, self.addon_to_remove.macro.filename + ".manifest"
- )
- if os.path.exists(manifest_file):
- with open(manifest_file, "r", encoding="utf-8") as f:
- manifest_data = f.read()
- manifest = json.loads(manifest_data)
- manifest.append(manifest_file) # Remove the manifest itself as well
- return manifest
- files_to_remove = [self.addon_to_remove.macro.filename]
- if self.addon_to_remove.macro.icon:
- files_to_remove.append(self.addon_to_remove.macro.icon)
- if self.addon_to_remove.macro.xpm:
- files_to_remove.append(self.addon_to_remove.macro.name.replace(" ", "_") + "_icon.xpm")
- for f in self.addon_to_remove.macro.other_files:
- files_to_remove.append(f)
- return files_to_remove
-
- @staticmethod
- def _cleanup_directories(directories):
- """Clean up any extra directories that are leftover and are empty"""
- for directory in directories:
- if os.path.isdir(directory):
- utils.remove_directory_if_empty(directory)
diff --git a/src/Mod/AddonManager/addonmanager_uninstaller_gui.py b/src/Mod/AddonManager/addonmanager_uninstaller_gui.py
deleted file mode 100644
index 86f6b9a942..0000000000
--- a/src/Mod/AddonManager/addonmanager_uninstaller_gui.py
+++ /dev/null
@@ -1,137 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022 FreeCAD Project Association *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""GUI functions for uninstalling an Addon or Macro."""
-
-import FreeCAD
-import FreeCADGui
-
-from PySide import QtCore, QtWidgets
-
-from addonmanager_uninstaller import AddonUninstaller, MacroUninstaller
-import addonmanager_utilities as utils
-
-translate = FreeCAD.Qt.translate
-
-
-class AddonUninstallerGUI(QtCore.QObject):
- """User interface for uninstalling an Addon: asks for confirmation, displays a progress dialog,
- displays completion and/or error dialogs, and emits the finished() signal when all work is
- complete."""
-
- finished = QtCore.Signal()
-
- def __init__(self, addon_to_remove):
- super().__init__()
- self.addon_to_remove = addon_to_remove
- if hasattr(self.addon_to_remove, "macro") and self.addon_to_remove.macro is not None:
- self.uninstaller = MacroUninstaller(self.addon_to_remove)
- else:
- self.uninstaller = AddonUninstaller(self.addon_to_remove)
- self.uninstaller.success.connect(self._succeeded)
- self.uninstaller.failure.connect(self._failed)
- self.worker_thread = QtCore.QThread()
- self.uninstaller.moveToThread(self.worker_thread)
- self.uninstaller.finished.connect(self.worker_thread.quit)
- self.worker_thread.started.connect(self.uninstaller.run)
- self.progress_dialog = None
- self.dialog_timer = QtCore.QTimer()
- self.dialog_timer.timeout.connect(self._show_progress_dialog)
- self.dialog_timer.setSingleShot(True)
- self.dialog_timer.setInterval(1000) # Can override from external (e.g. testing) code
-
- def run(self):
- """Begin the user interaction: asynchronous, only blocks while showing the initial modal
- confirmation dialog."""
- ok_to_proceed = self._confirm_uninstallation()
- if not ok_to_proceed:
- self._finalize()
- return
-
- self.dialog_timer.start()
- self._run_uninstaller()
-
- def _confirm_uninstallation(self) -> bool:
- """Present a modal dialog asking the user if they really want to uninstall. Returns True to
- continue with the uninstallation, or False to stop the process."""
- confirm = QtWidgets.QMessageBox.question(
- utils.get_main_am_window(),
- translate("AddonsInstaller", "Confirm remove"),
- translate("AddonsInstaller", "Are you sure you want to uninstall {}?").format(
- self.addon_to_remove.display_name
- ),
- QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel,
- )
- return confirm == QtWidgets.QMessageBox.Yes
-
- def _show_progress_dialog(self):
- self.progress_dialog = QtWidgets.QMessageBox(
- QtWidgets.QMessageBox.NoIcon,
- translate("AddonsInstaller", "Removing Addon"),
- translate("AddonsInstaller", "Removing {}").format(self.addon_to_remove.display_name)
- + "...",
- QtWidgets.QMessageBox.Cancel,
- parent=utils.get_main_am_window(),
- )
- self.progress_dialog.rejected.connect(self._cancel_removal)
- self.progress_dialog.show()
-
- def _run_uninstaller(self):
- self.worker_thread.start()
-
- def _cancel_removal(self):
- """Ask the QThread to interrupt. Probably has no effect, most of the work is in a single OS
- call."""
- self.worker_thread.requestInterruption()
-
- def _succeeded(self, addon):
- """Callback for successful removal"""
- self.dialog_timer.stop()
- if self.progress_dialog:
- self.progress_dialog.hide()
- QtWidgets.QMessageBox.information(
- utils.get_main_am_window(),
- translate("AddonsInstaller", "Uninstall complete"),
- translate("AddonInstaller", "Finished removing {}").format(addon.display_name),
- )
- self._finalize()
-
- def _failed(self, addon, message):
- """Callback for failed or partially failed removal"""
- self.dialog_timer.stop()
- if self.progress_dialog:
- self.progress_dialog.hide()
- QtWidgets.QMessageBox.critical(
- utils.get_main_am_window(),
- translate("AddonsInstaller", "Uninstall failed"),
- translate("AddonInstaller", "Failed to remove some files") + ":\n" + message,
- )
- self._finalize()
-
- def _finalize(self):
- """Clean up and emit finished signal"""
- if self.worker_thread.isRunning():
- self.worker_thread.requestInterruption()
- self.worker_thread.quit()
- self.worker_thread.wait(500)
- self.finished.emit()
diff --git a/src/Mod/AddonManager/addonmanager_update_all_gui.py b/src/Mod/AddonManager/addonmanager_update_all_gui.py
deleted file mode 100644
index 89fd775ea7..0000000000
--- a/src/Mod/AddonManager/addonmanager_update_all_gui.py
+++ /dev/null
@@ -1,242 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2025 FreeCAD project association AISBL *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# *
. *
-# * *
-# ***************************************************************************
-
-"""Class to manage the display of an Update All dialog."""
-
-from enum import IntEnum, auto
-import os
-from typing import List
-
-import FreeCAD
-import FreeCADGui
-
-from PySide import QtCore, QtWidgets
-
-from Addon import Addon
-
-from addonmanager_installer import AddonInstaller, MacroInstaller
-
-translate = FreeCAD.Qt.translate
-
-# pylint: disable=too-few-public-methods,too-many-instance-attributes
-
-
-class UpdaterFactory:
- """A factory class for generating updaters. Mainly exists to allow easily mocking
- those updaters during testing. A replacement class need only provide a
- "get_updater" function that returns mock updater objects. Those objects must be
- QObjects with a run() function and a finished signal."""
-
- def __init__(self, addons):
- self.addons = addons
-
- def get_updater(self, addon):
- """Get an updater for this addon (either a MacroInstaller or an
- AddonInstaller)"""
- if addon.macro is not None:
- return MacroInstaller(addon)
- return AddonInstaller(addon, self.addons)
-
-
-class AddonStatus(IntEnum):
- """The current status of the installation process for a given addon"""
-
- WAITING = auto()
- INSTALLING = auto()
- SUCCEEDED = auto()
- FAILED = auto()
-
- def ui_string(self):
- """Get the string that the UI should show for this status"""
- if self.value == AddonStatus.WAITING:
- return ""
- if self.value == AddonStatus.INSTALLING:
- return translate("AddonsInstaller", "Installing") + "..."
- if self.value == AddonStatus.SUCCEEDED:
- return translate("AddonsInstaller", "Succeeded")
- if self.value == AddonStatus.FAILED:
- return translate("AddonsInstaller", "Failed")
- return "[INTERNAL ERROR]"
-
-
-class UpdateAllGUI(QtCore.QObject):
- """A GUI to display and manage an "update all" process."""
-
- finished = QtCore.Signal()
- addon_updated = QtCore.Signal(object)
-
- index_role = QtCore.Qt.UserRole + 1
-
- def __init__(self, addons: List[Addon]):
- super().__init__()
- self.addons = addons
- self.dialog = FreeCADGui.PySideUic.loadUi(
- os.path.join(os.path.dirname(__file__), "update_all.ui")
- )
- self.row_map = {}
- self.in_process_row = None
- self.active_installer = None
- self.addons_with_update: List[Addon] = []
- self.updater_factory = UpdaterFactory(addons)
- self.worker_thread = None
- self.running = False
- self.cancelled = False
-
- def run(self):
- """Run the Update All process. Blocks until updates are complete or
- cancelled."""
- self.running = True
- self._setup_dialog()
- self.dialog.show()
- self._process_next_update()
-
- def _setup_dialog(self):
- """Prepare the dialog for display"""
- self.dialog.rejected.connect(self._cancel_installation)
- self.dialog.tableWidget.clear()
- self.in_process_row = None
- self.row_map = {}
- self._setup_empty_table()
- counter = 0
- for addon in self.addons:
- if addon.status() == Addon.Status.UPDATE_AVAILABLE:
- self._add_addon_to_table(addon, counter)
- self.addons_with_update.append(addon)
- counter += 1
-
- def _cancel_installation(self):
- self.cancelled = True
- if self.worker_thread and self.worker_thread.isRunning():
- self.worker_thread.requestInterruption()
-
- def _setup_empty_table(self):
- self.dialog.tableWidget.setColumnCount(4)
- self.dialog.tableWidget.horizontalHeader().setSectionResizeMode(
- 0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents
- )
- self.dialog.tableWidget.horizontalHeader().setSectionResizeMode(
- 1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents
- )
- self.dialog.tableWidget.horizontalHeader().setSectionResizeMode(
- 2, QtWidgets.QHeaderView.ResizeMode.ResizeToContents
- )
- self.dialog.tableWidget.horizontalHeader().setSectionResizeMode(
- 3, QtWidgets.QHeaderView.ResizeMode.Stretch
- )
-
- def _add_addon_to_table(self, addon: Addon, index: int):
- """Add the given addon to the list, storing its index as user data in the first column"""
- new_row = self.dialog.tableWidget.rowCount()
- self.dialog.tableWidget.setRowCount(new_row + 1)
- new_item = QtWidgets.QTableWidgetItem(addon.display_name)
- new_item.setData(UpdateAllGUI.index_role, index) # Only first item in each row needs data()
- self.dialog.tableWidget.setItem(new_row, 0, new_item)
- if addon.installed_metadata and addon.installed_metadata.version:
- self.dialog.tableWidget.setItem(
- new_row, 1, QtWidgets.QTableWidgetItem(str(addon.installed_metadata.version))
- )
- self.dialog.tableWidget.setItem(new_row, 2, QtWidgets.QTableWidgetItem(""))
- self.dialog.tableWidget.setItem(new_row, 3, QtWidgets.QTableWidgetItem(""))
- self.row_map[addon.name] = new_row
-
- def _update_addon_status(self, row: int, status: AddonStatus):
- """Update the GUI to reflect this addon's new status."""
- self.dialog.tableWidget.item(row, 2).setText(status.ui_string())
- if status == AddonStatus.SUCCEEDED and self.addons[row].metadata:
- self.dialog.tableWidget.item(row, 2).setText(status.ui_string() + " →")
- index = self.dialog.tableWidget.item(row, 0).data(UpdateAllGUI.index_role)
- addon = self.addons[index]
- if addon.metadata and addon.metadata.version:
- self.dialog.tableWidget.item(row, 3).setText(str(addon.metadata.version))
-
- def _process_next_update(self):
- """Grab the next addon in the list and start its updater."""
- if self.addons_with_update:
- addon = self.addons_with_update.pop(0)
- self.in_process_row = self.row_map[addon.name] if addon.name in self.row_map else None
- self._update_addon_status(self.in_process_row, AddonStatus.INSTALLING)
- self.dialog.tableWidget.scrollToItem(
- self.dialog.tableWidget.item(self.in_process_row, 0)
- )
- self.active_installer = self.updater_factory.get_updater(addon)
- self._launch_active_installer()
- else:
- self._finalize()
-
- def _launch_active_installer(self):
- """Set up and run the active installer in a new thread."""
-
- self.active_installer.success.connect(self._update_succeeded)
- self.active_installer.failure.connect(self._update_failed)
- self.active_installer.finished.connect(self._update_finished)
-
- self.worker_thread = QtCore.QThread()
- self.active_installer.moveToThread(self.worker_thread)
- self.worker_thread.started.connect(self.active_installer.run)
- self.worker_thread.start()
-
- def _update_succeeded(self, addon):
- """Callback for a successful update"""
- self._update_addon_status(self.row_map[addon.name], AddonStatus.SUCCEEDED)
- self.addon_updated.emit(addon)
-
- def _update_failed(self, addon):
- """Callback for a failed update"""
- self._update_addon_status(self.row_map[addon.name], AddonStatus.FAILED)
-
- def _update_finished(self):
- """Callback for updater that has finished all its work"""
- if self.worker_thread is not None and self.worker_thread.isRunning():
- self.worker_thread.quit()
- self.worker_thread.wait()
- self.addon_updated.emit(self.active_installer.addon_to_install)
- if not self.cancelled:
- self._process_next_update()
- else:
- self._setup_cancelled_state()
-
- def _finalize(self):
- """No more updates, clean up and shut down"""
- if self.worker_thread is not None and self.worker_thread.isRunning():
- self.worker_thread.quit()
- self.worker_thread.wait()
- text = translate("Addons installer", "Finished updating the following addons")
- self._set_dialog_to_final_state(text)
- self.running = False
- self.finished.emit()
-
- def _setup_cancelled_state(self):
- text1 = translate("AddonsInstaller", "Update was cancelled")
- text2 = translate("AddonsInstaller", "some addons may have been updated")
- self._set_dialog_to_final_state(text1 + ": " + text2)
- self.running = False
- self.finished.emit()
-
- def _set_dialog_to_final_state(self, new_content):
- self.dialog.buttonBox.clear()
- self.dialog.buttonBox.addButton(QtWidgets.QDialogButtonBox.Close)
- self.dialog.label.setText(new_content)
-
- def is_running(self):
- """True if the thread is running, and False if not"""
- return self.running
diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py
deleted file mode 100644
index 48cc6f0832..0000000000
--- a/src/Mod/AddonManager/addonmanager_utilities.py
+++ /dev/null
@@ -1,599 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2023 FreeCAD Project Association *
-# * Copyright (c) 2018 Gaël Écorchard
*
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# * . *
-# * *
-# ***************************************************************************
-
-"""Utilities to work across different platforms, providers and python versions"""
-
-# pylint: disable=deprecated-module, ungrouped-imports
-
-from datetime import datetime
-from typing import Optional, Any, List
-import os
-import platform
-import shutil
-import stat
-import subprocess
-import time
-import re
-import ctypes
-
-from urllib.parse import urlparse
-
-try:
- from PySide import QtCore, QtGui, QtWidgets
-except ImportError:
- try:
- from PySide6 import QtCore, QtGui, QtWidgets
- except ImportError:
- from PySide2 import QtCore, QtGui, QtWidgets
-
-import addonmanager_freecad_interface as fci
-
-try:
- from freecad.utils import get_python_exe
-except ImportError:
-
- def get_python_exe():
- """Use shutil.which to find python executable"""
- return shutil.which("python")
-
-
-if fci.FreeCADGui:
-
- # If the GUI is up, we can use the NetworkManager to handle our downloads. If there is no event
- # loop running this is not possible, so fall back to requests (if available), or the native
- # Python urllib.request (if requests is not available).
- import NetworkManager # Requires an event loop, so is only available with the GUI
-
- requests = None
- ssl = None
- urllib = None
-else:
- NetworkManager = None
- try:
- import requests
-
- ssl = None
- urllib = None
- except ImportError:
- requests = None
- import urllib.request
- import ssl
-
-if fci.FreeCADGui:
- loadUi = fci.loadUi
-else:
- has_loader = False
- try:
- from PySide6.QtUiTools import QUiLoader
-
- has_loader = True
- except ImportError:
- try:
- from PySide2.QtUiTools import QUiLoader
-
- has_loader = True
- except ImportError:
-
- def loadUi(ui_file: str):
- """If there are no available versions of QtUiTools, then raise an error if this
- method is used."""
- raise RuntimeError("Cannot use QUiLoader without PySide or FreeCAD")
-
- if has_loader:
-
- def loadUi(ui_file: str) -> QtWidgets.QWidget:
- """Load a Qt UI from an on-disk file."""
- q_ui_file = QtCore.QFile(ui_file)
- q_ui_file.open(QtCore.QFile.OpenModeFlag.ReadOnly)
- loader = QUiLoader()
- return loader.load(ui_file)
-
-
-# @package AddonManager_utilities
-# \ingroup ADDONMANAGER
-# \brief Utilities to work across different platforms, providers and python versions
-# @{
-
-
-translate = fci.translate
-
-
-class ProcessInterrupted(RuntimeError):
- """An interruption request was received and the process killed because of it."""
-
-
-def symlink(source, link_name):
- """Creates a symlink of a file, if possible. Note that it fails on most modern Windows
- installations"""
-
- if os.path.exists(link_name) or os.path.lexists(link_name):
- pass
- else:
- os_symlink = getattr(os, "symlink", None)
- if callable(os_symlink):
- os_symlink(source, link_name)
- else:
- # NOTE: This does not work on most normal Windows 10 and later installations, unless
- # developer mode is turned on. Make sure to catch any exception thrown and have a
- # fallback plan.
- csl = ctypes.windll.kernel32.CreateSymbolicLinkW
- csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
- csl.restype = ctypes.c_ubyte
- flags = 1 if os.path.isdir(source) else 0
- # set the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag
- # (see https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10)
- flags += 2
- if csl(link_name, source, flags) == 0:
- raise ctypes.WinError()
-
-
-def rmdir(path: str) -> bool:
- """Remove a directory or symlink, even if it is read-only."""
- try:
- if os.path.islink(path):
- os.unlink(path) # Remove symlink
- else:
- # NOTE: the onerror argument was deprecated in Python 3.12, replaced by onexc -- replace
- # when earlier versions are no longer supported.
- shutil.rmtree(path, onerror=remove_readonly)
- except (WindowsError, PermissionError, OSError):
- return False
- return True
-
-
-def remove_readonly(func, path, _) -> None:
- """Remove a read-only file."""
-
- os.chmod(path, stat.S_IWRITE)
- func(path)
-
-
-def update_macro_details(old_macro, new_macro):
- """Update a macro with information from another one
-
- Update a macro with information from another one, supposedly the same but
- from a different source. The first source is supposed to be git, the second
- one the wiki.
- """
-
- if old_macro.on_git and new_macro.on_git:
- fci.Console.PrintLog(
- f'The macro "{old_macro.name}" is present twice in github, please report'
- )
- # We don't report macros present twice on the wiki because a link to a
- # macro is considered as a macro. For example, 'Perpendicular To Wire'
- # appears twice, as of 2018-05-05).
- old_macro.on_wiki = new_macro.on_wiki
- for attr in ["desc", "url", "code"]:
- if not hasattr(old_macro, attr):
- setattr(old_macro, attr, getattr(new_macro, attr))
-
-
-def remove_directory_if_empty(dir_to_remove):
- """Remove the directory if it is empty, with one exception: the directory returned by
- FreeCAD.getUserMacroDir(True) will not be removed even if it is empty."""
-
- if dir_to_remove == fci.DataPaths().macro_dir:
- return
- if not os.listdir(dir_to_remove):
- os.rmdir(dir_to_remove)
-
-
-def restart_freecad():
- """Shuts down and restarts FreeCAD"""
-
- if not QtCore or not QtWidgets:
- return
-
- args = QtWidgets.QApplication.arguments()[1:]
- if fci.FreeCADGui.getMainWindow().close():
- QtCore.QProcess.startDetached(QtWidgets.QApplication.applicationFilePath(), args)
-
-
-def get_zip_url(repo):
- """Returns the location of a zip file from a repo, if available"""
-
- parsed_url = urlparse(repo.url)
- if parsed_url.netloc == "github.com":
- return f"{repo.url}/archive/{repo.branch}.zip"
- if parsed_url.netloc in ["gitlab.com", "framagit.org", "salsa.debian.org"]:
- return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip"
- if parsed_url.netloc in ["codeberg.org"]:
- return f"{repo.url}/archive/{repo.branch}.zip"
- fci.Console.PrintLog(
- "Debug: addonmanager_utilities.get_zip_url: Unknown git host fetching zip URL:"
- + parsed_url.netloc
- + "\n"
- )
- return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip"
-
-
-def recognized_git_location(repo) -> bool:
- """Returns whether this repo is based at a known git repo location: works with GitHub, gitlab,
- framagit, and salsa.debian.org"""
-
- parsed_url = urlparse(repo.url)
- return parsed_url.netloc in [
- "github.com",
- "gitlab.com",
- "framagit.org",
- "salsa.debian.org",
- "codeberg.org",
- ]
-
-
-def construct_git_url(repo, filename):
- """Returns a direct download link to a file in an online Git repo"""
-
- parsed_url = urlparse(repo.url)
- repo_url = repo.url[:-4] if repo.url.endswith(".git") else repo.url
- if parsed_url.netloc == "github.com":
- return f"{repo_url}/raw/{repo.branch}/{filename}"
- if parsed_url.netloc in ["gitlab.com", "framagit.org", "salsa.debian.org"]:
- return f"{repo_url}/-/raw/{repo.branch}/{filename}"
- if parsed_url.netloc in ["codeberg.org"]:
- return f"{repo_url}/raw/branch/{repo.branch}/{filename}"
- fci.Console.PrintLog(
- "Debug: addonmanager_utilities.construct_git_url: Unknown git host:"
- + parsed_url.netloc
- + f" for file {filename}\n"
- )
- # Assume it's some kind of local GitLab instance...
- return f"{repo_url}/-/raw/{repo.branch}/{filename}"
-
-
-def get_readme_url(repo):
- """Returns the location of a readme file"""
-
- return construct_git_url(repo, "README.md")
-
-
-def get_metadata_url(url):
- """Returns the location of a package.xml metadata file"""
-
- return construct_git_url(url, "package.xml")
-
-
-def get_desc_regex(repo):
- """Returns a regex string that extracts a WB description to be displayed in the description
- panel of the Addon manager, if the README could not be found"""
-
- parsed_url = urlparse(repo.url)
- if parsed_url.netloc == "github.com":
- return r''
- if parsed_url.netloc in ["codeberg.org"]:
- return r''
-
-
-def get_readme_html_url(repo):
- """Returns the location of a html file containing readme"""
-
- parsed_url = urlparse(repo.url)
- if parsed_url.netloc == "github.com":
- return f"{repo.url}/blob/{repo.branch}/README.md"
- if parsed_url.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]:
- return f"{repo.url}/-/blob/{repo.branch}/README.md"
- if parsed_url.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]:
- return f"{repo.url}/raw/branch/{repo.branch}/README.md"
- fci.Console.PrintLog("Unrecognized git repo location '' -- guessing it is a GitLab instance...")
- return f"{repo.url}/-/blob/{repo.branch}/README.md"
-
-
-def is_darkmode() -> bool:
- """Heuristics to determine if we are in a darkmode stylesheet"""
- pl = fci.FreeCADGui.getMainWindow().palette()
- return pl.color(QtGui.QPalette.Window).lightness() < 128
-
-
-def warning_color_string() -> str:
- """A shade of red, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
- return "rgb(255,105,97)" if is_darkmode() else "rgb(215,0,21)"
-
-
-def bright_color_string() -> str:
- """A shade of green, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
- return "rgb(48,219,91)" if is_darkmode() else "rgb(36,138,61)"
-
-
-def attention_color_string() -> str:
- """A shade of orange, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
- return "rgb(255,179,64)" if is_darkmode() else "rgb(255,149,0)"
-
-
-def get_assigned_string_literal(line: str) -> Optional[str]:
- """Look for a line of the form my_var = "A string literal" and return the string literal.
- If the assignment is of a floating point value, that value is converted to a string
- and returned. If neither is true, returns None."""
-
- string_search_regex = re.compile(r"\s*(['\"])(.*)\1")
- _, _, after_equals = line.partition("=")
- match = re.match(string_search_regex, after_equals)
- if match:
- return str(match.group(2))
- if is_float(after_equals):
- return str(after_equals).strip()
- return None
-
-
-def get_macro_version_from_file(filename: str) -> str:
- """Get the version of the macro from a local macro file. Supports strings, ints, and floats,
- as well as a reference to __date__"""
-
- date = ""
- with open(filename, errors="ignore", encoding="utf-8") as f:
- line_counter = 0
- max_lines_to_scan = 200
- while line_counter < max_lines_to_scan:
- line_counter += 1
- line = f.readline()
- if not line: # EOF
- break
- if line.lower().startswith("__version__"):
- match = get_assigned_string_literal(line)
- if match:
- return match
- if "__date__" in line.lower():
- # Don't do any real syntax checking, just assume the line is something
- # like __version__ = __date__
- if date:
- return date
- # pylint: disable=line-too-long,consider-using-f-string
- fci.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Macro {} specified '__version__ = __date__' prior to setting a value for __date__".format(
- filename
- ),
- )
- )
- elif line.lower().startswith("__date__"):
- match = get_assigned_string_literal(line)
- if match:
- date = match
- return ""
-
-
-def update_macro_installation_details(repo) -> None:
- """Determine if a given macro is installed, either in its plain name,
- or prefixed with "Macro_" """
- if repo is None or not hasattr(repo, "macro") or repo.macro is None:
- fci.Console.PrintLog("Requested macro details for non-macro object\n")
- return
- test_file_one = os.path.join(fci.DataPaths().macro_dir, repo.macro.filename)
- test_file_two = os.path.join(fci.DataPaths().macro_dir, "Macro_" + repo.macro.filename)
- if os.path.exists(test_file_one):
- repo.updated_timestamp = os.path.getmtime(test_file_one)
- repo.installed_version = get_macro_version_from_file(test_file_one)
- elif os.path.exists(test_file_two):
- repo.updated_timestamp = os.path.getmtime(test_file_two)
- repo.installed_version = get_macro_version_from_file(test_file_two)
- else:
- return
-
-
-# Borrowed from Stack Overflow:
-# https://stackoverflow.com/questions/736043/checking-if-a-string-can-be-converted-to-float
-def is_float(element: Any) -> bool:
- """Determine whether a given item can be converted to a floating-point number"""
- try:
- float(element)
- return True
- except ValueError:
- return False
-
-
-def get_pip_target_directory():
- """Get the default location to install new pip packages"""
- major, minor, _ = platform.python_version_tuple()
- snap_package = os.getenv("SNAP_REVISION")
-
- if snap_package:
- import site
-
- vendor_path = site.getusersitepackages()
- else:
- vendor_path = os.path.normpath(
- os.path.join(
- fci.DataPaths().mod_dir, "..", "AdditionalPythonPackages", f"py{major}{minor}"
- )
- )
- return vendor_path
-
-
-def get_cache_file_name(file: str) -> str:
- """Get the full path to a cache file with a given name."""
- cache_path = fci.DataPaths().cache_dir
- am_path = os.path.join(cache_path, "AddonManager")
- os.makedirs(am_path, exist_ok=True)
- return os.path.join(am_path, file)
-
-
-def blocking_get(url: str, method=None) -> bytes:
- """Wrapper around three possible ways of accessing data, depending on the current run mode and
- Python installation. Blocks until complete, and returns the text results of the call if it
- succeeded, or an empty string if it failed, or returned no data. The method argument is
- provided mainly for testing purposes."""
- p = b""
- if (
- fci.FreeCADGui
- and method is None
- or method == "networkmanager"
- and NetworkManager is not None
- ):
- NetworkManager.InitializeNetworkManager()
- p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url, 10000) # 10 second timeout
- if p:
- try:
- p = p.data()
- except AttributeError:
- pass
- elif requests and method is None or method == "requests":
- response = requests.get(url)
- if response.status_code == 200:
- p = response.content
- else:
- ctx = ssl.create_default_context()
- with urllib.request.urlopen(url, context=ctx) as f:
- p = f.read()
- return p
-
-
-def run_interruptable_subprocess(args, timeout_secs: int = 10) -> subprocess.CompletedProcess:
- """Wrap subprocess call so it can be interrupted gracefully."""
- creation_flags = 0
- if hasattr(subprocess, "CREATE_NO_WINDOW"):
- # Added in Python 3.7 -- only used on Windows
- creation_flags = subprocess.CREATE_NO_WINDOW
- try:
- p = subprocess.Popen(
- args,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- creationflags=creation_flags,
- text=True,
- encoding="utf-8",
- )
- except OSError as e:
- raise subprocess.CalledProcessError(-1, args, "", e.strerror)
- stdout = ""
- stderr = ""
- return_code = None
- start_time = time.time()
- while return_code is None:
- try:
- # one second timeout allows interrupting the run once per second
- stdout, stderr = p.communicate(timeout=1)
- return_code = p.returncode
- except subprocess.TimeoutExpired as timeout_exception:
- if (
- hasattr(QtCore, "QThread")
- and QtCore.QThread.currentThread().isInterruptionRequested()
- ):
- p.kill()
- raise ProcessInterrupted() from timeout_exception
- if time.time() - start_time >= timeout_secs: # The real timeout
- p.kill()
- stdout, stderr = p.communicate()
- return_code = -1
- if return_code is None or return_code != 0:
- raise subprocess.CalledProcessError(
- return_code if return_code is not None else -1, args, stdout, stderr
- )
- return subprocess.CompletedProcess(args, return_code, stdout, stderr)
-
-
-def process_date_string_to_python_datetime(date_string: str) -> datetime:
- """For modern macros the expected date format is ISO 8601, YYYY-MM-DD. For older macros this
- standard was not always used, and various orderings and separators were used. This function
- tries to match the majority of those older macros. Commonly-used separators are periods,
- slashes, and dashes."""
-
- def raise_error(bad_string: str, root_cause: Exception = None):
- raise ValueError(
- f"Unrecognized date string '{bad_string}' (expected YYYY-MM-DD)"
- ) from root_cause
-
- split_result = re.split(r"[ ./-]+", date_string.strip())
- if len(split_result) != 3:
- raise_error(date_string)
-
- try:
- split_result = [int(x) for x in split_result]
- # The earliest possible year an addon can be created or edited is 2001:
- if split_result[0] > 2000:
- return datetime(split_result[0], split_result[1], split_result[2])
- if split_result[2] > 2000:
- # Generally speaking it's not possible to distinguish between DD-MM and MM-DD, so try
- # the first, and only if that fails try the second
- if split_result[1] <= 12:
- return datetime(split_result[2], split_result[1], split_result[0])
- return datetime(split_result[2], split_result[0], split_result[1])
- raise ValueError(f"Invalid year in date string '{date_string}'")
- except ValueError as exception:
- raise_error(date_string, exception)
-
-
-def get_main_am_window():
- """Find the Addon Manager's main window in the Qt widget hierarchy."""
- windows = QtWidgets.QApplication.topLevelWidgets()
- for widget in windows:
- if widget.objectName() == "AddonManager_Main_Window":
- return widget
- # If there is no main AM window, we may be running unit tests: see if the Test Runner window
- # exists:
- for widget in windows:
- if widget.objectName() == "TestGui__UnitTest":
- return widget
- # If we still didn't find it, try to locate the main FreeCAD window:
- for widget in windows:
- if hasattr(widget, "centralWidget"):
- return widget.centralWidget()
- # Why is this code even getting called?
- return None
-
-
-def remove_options_and_arg(call_args: List[str], deny_args: List[str]) -> List[str]:
- """Removes a set of options and their only argument from a pip call.
- This is necessary as the pip binary in the snap package is called with
- the --user option, which is not compatible with some other options such
- as --target and --path. We then have to remove e.g. target --path and
- its argument, if present."""
- for deny_arg in deny_args:
- try:
- index = call_args.index(deny_arg)
- del call_args[index : index + 2] # The option and its argument
- except ValueError:
- pass
- return call_args
-
-
-def create_pip_call(args: List[str]) -> List[str]:
- """Choose the correct mechanism for calling pip on each platform. It currently supports
- either `python -m pip` (most environments) or `pip` (Snap packages). Returns a list
- of arguments suitable for passing directly to subprocess.Popen and related functions."""
- snap_package = os.getenv("SNAP_REVISION")
- appimage = os.getenv("APPIMAGE")
- if snap_package:
- args = remove_options_and_arg(args, ["--target", "--path"])
- call_args = ["pip", "--disable-pip-version-check"]
- call_args.extend(args)
- elif appimage:
- python_exe = fci.DataPaths().home_dir + "bin/python"
- call_args = [python_exe, "-m", "pip", "--disable-pip-version-check"]
- call_args.extend(args)
- else:
- python_exe = get_python_exe()
- if not python_exe:
- raise RuntimeError("Could not locate Python executable on this system")
- call_args = [python_exe, "-m", "pip", "--disable-pip-version-check"]
- call_args.extend(args)
- return call_args
diff --git a/src/Mod/AddonManager/addonmanager_workers_installation.py b/src/Mod/AddonManager/addonmanager_workers_installation.py
deleted file mode 100644
index 635b2650ab..0000000000
--- a/src/Mod/AddonManager/addonmanager_workers_installation.py
+++ /dev/null
@@ -1,315 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2023 FreeCAD Project Association *
-# * Copyright (c) 2019 Yorik van Havre *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# * . *
-# * *
-# ***************************************************************************
-
-"""Worker thread classes for Addon Manager installation and removal"""
-
-# pylint: disable=c-extension-no-member,too-few-public-methods,too-many-instance-attributes
-
-import json
-import os
-from typing import Dict
-from enum import Enum, auto
-import xml.etree.ElementTree
-
-from PySide import QtCore
-
-import FreeCAD
-import addonmanager_utilities as utils
-from addonmanager_metadata import MetadataReader
-from Addon import Addon
-import NetworkManager
-import addonmanager_freecad_interface as fci
-
-translate = FreeCAD.Qt.translate
-
-# @package AddonManager_workers
-# \ingroup ADDONMANAGER
-# \brief Multithread workers for the addon manager
-# @{
-
-
-class UpdateMetadataCacheWorker(QtCore.QThread):
- """Scan through all available packages and see if our local copy of package.xml needs to be
- updated"""
-
- progress_made = QtCore.Signal(str, int, int)
- package_updated = QtCore.Signal(Addon)
-
- class RequestType(Enum):
- """The type of item being downloaded."""
-
- PACKAGE_XML = auto()
- METADATA_TXT = auto()
- REQUIREMENTS_TXT = auto()
- ICON = auto()
-
- def __init__(self, repos):
-
- QtCore.QThread.__init__(self)
- self.repos = repos
- self.requests: Dict[int, (Addon, UpdateMetadataCacheWorker.RequestType)] = {}
- NetworkManager.AM_NETWORK_MANAGER.completed.connect(self.download_completed)
- self.requests_completed = 0
- self.total_requests = 0
- self.store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
- FreeCAD.Console.PrintLog(f"Storing Addon Manager cache data in {self.store}\n")
- self.updated_repos = set()
- self.remote_cache_data = {}
-
- def run(self):
- """Not usually called directly: instead, create an instance and call its
- start() function to spawn a new thread."""
-
- self.update_from_remote_cache()
-
- current_thread = QtCore.QThread.currentThread()
-
- for repo in self.repos:
- if repo.name in self.remote_cache_data:
- self.update_addon_from_remote_cache_data(repo)
- elif not repo.macro and repo.url and utils.recognized_git_location(repo):
- # package.xml
- index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
- utils.construct_git_url(repo, "package.xml")
- )
- self.requests[index] = (
- repo,
- UpdateMetadataCacheWorker.RequestType.PACKAGE_XML,
- )
- self.total_requests += 1
-
- # metadata.txt
- index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
- utils.construct_git_url(repo, "metadata.txt")
- )
- self.requests[index] = (
- repo,
- UpdateMetadataCacheWorker.RequestType.METADATA_TXT,
- )
- self.total_requests += 1
-
- # requirements.txt
- index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
- utils.construct_git_url(repo, "requirements.txt")
- )
- self.requests[index] = (
- repo,
- UpdateMetadataCacheWorker.RequestType.REQUIREMENTS_TXT,
- )
- self.total_requests += 1
-
- while self.requests:
- if current_thread.isInterruptionRequested():
- for request in self.requests:
- NetworkManager.AM_NETWORK_MANAGER.abort(request)
- return
- # 50 ms maximum between checks for interruption
- QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
-
- # This set contains one copy of each of the repos that got some kind of data in
- # this process. For those repos, tell the main Addon Manager code that it needs
- # to update its copy of the repo, and redraw its information.
- for repo in self.updated_repos:
- self.package_updated.emit(repo)
-
- def update_from_remote_cache(self) -> None:
- """Pull the data on the official repos from a remote cache site (usually
- https://freecad.org/addons/addon_cache.json)"""
- data_source = fci.Preferences().get("AddonsCacheURL")
- try:
- fetch_result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(data_source, 5000)
- if fetch_result:
- self.remote_cache_data = json.loads(fetch_result.data())
- else:
- fci.Console.PrintWarning(
- f"Failed to read from {data_source}. Continuing without remote cache...\n"
- )
- except RuntimeError:
- # If the remote cache can't be fetched, we continue anyway
- pass
-
- def update_addon_from_remote_cache_data(self, addon: Addon):
- """Given a repo that exists in the remote cache, load in its metadata."""
- fci.Console.PrintLog(f"Used remote cache data for {addon.name} metadata\n")
- if "package.xml" in self.remote_cache_data[addon.name]:
- self.process_package_xml(addon, self.remote_cache_data[addon.name]["package.xml"])
- if "requirements.txt" in self.remote_cache_data[addon.name]:
- self.process_requirements_txt(
- addon, self.remote_cache_data[addon.name]["requirements.txt"]
- )
- if "metadata.txt" in self.remote_cache_data[addon.name]:
- self.process_metadata_txt(addon, self.remote_cache_data[addon.name]["metadata.txt"])
-
- def download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> None:
- """Callback for handling a completed metadata file download."""
- if index in self.requests:
- self.requests_completed += 1
- request = self.requests.pop(index)
- if code == 200: # HTTP success
- self.updated_repos.add(request[0]) # mark this repo as updated
- file = "unknown"
- if request[1] == UpdateMetadataCacheWorker.RequestType.PACKAGE_XML:
- self.process_package_xml(request[0], data)
- file = "package.xml"
- elif request[1] == UpdateMetadataCacheWorker.RequestType.METADATA_TXT:
- self.process_metadata_txt(request[0], data)
- file = "metadata.txt"
- elif request[1] == UpdateMetadataCacheWorker.RequestType.REQUIREMENTS_TXT:
- self.process_requirements_txt(request[0], data)
- file = "requirements.txt"
- elif request[1] == UpdateMetadataCacheWorker.RequestType.ICON:
- self.process_icon(request[0], data)
- file = "icon"
- message = translate("AddonsInstaller", "Downloaded {} for {}").format(
- file, request[0].display_name
- )
- self.progress_made.emit(message, self.requests_completed, self.total_requests)
-
- def process_package_xml(self, repo: Addon, data: QtCore.QByteArray):
- """Process the package.xml metadata file"""
- repo.repo_type = Addon.Kind.PACKAGE # By definition
- package_cache_directory = os.path.join(self.store, repo.name)
- if not os.path.exists(package_cache_directory):
- os.makedirs(package_cache_directory)
- new_xml_file = os.path.join(package_cache_directory, "package.xml")
- with open(new_xml_file, "w", encoding="utf-8") as f:
- string_data = self._ensure_string(data, repo.name, "package.xml")
- f.write(string_data)
- try:
- metadata = MetadataReader.from_file(new_xml_file)
- except xml.etree.ElementTree.ParseError:
- fci.Console.PrintWarning("An invalid or corrupted package.xml file was downloaded for")
- fci.Console.PrintWarning(f" {self.name}... ignoring the bad data.\n")
- return
- repo.set_metadata(metadata)
- FreeCAD.Console.PrintLog(f"Downloaded package.xml for {repo.name}\n")
-
- # Grab a new copy of the icon as well: we couldn't enqueue this earlier because
- # we didn't know the path to it, which is stored in the package.xml file.
- icon = repo.get_best_icon_relative_path()
-
- icon_url = utils.construct_git_url(repo, icon)
- index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(icon_url)
- self.requests[index] = (repo, UpdateMetadataCacheWorker.RequestType.ICON)
- self.total_requests += 1
-
- def _ensure_string(self, arbitrary_data, addon_name, file_name) -> str:
- if isinstance(arbitrary_data, str):
- return arbitrary_data
- if isinstance(arbitrary_data, QtCore.QByteArray):
- return self._decode_data(arbitrary_data.data(), addon_name, file_name)
- return self._decode_data(arbitrary_data, addon_name, file_name)
-
- def _decode_data(self, byte_data, addon_name, file_name) -> str:
- """UTF-8 decode data, and print an error message if that fails"""
-
- # For review and debugging purposes, store the file locally
- package_cache_directory = os.path.join(self.store, addon_name)
- if not os.path.exists(package_cache_directory):
- os.makedirs(package_cache_directory)
- new_xml_file = os.path.join(package_cache_directory, file_name)
- with open(new_xml_file, "wb") as f:
- f.write(byte_data)
-
- f = ""
- try:
- f = byte_data.decode("utf-8")
- except UnicodeDecodeError as e:
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Failed to decode {} file for Addon '{}'",
- ).format(file_name, addon_name)
- + "\n"
- )
- FreeCAD.Console.PrintWarning(str(e) + "\n")
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Any dependency information in this file will be ignored",
- )
- + "\n"
- )
- return f
-
- def process_metadata_txt(self, repo: Addon, data: QtCore.QByteArray):
- """Process the metadata.txt metadata file"""
- f = self._ensure_string(data, repo.name, "metadata.txt")
- lines = f.splitlines()
- for line in lines:
- if line.startswith("workbenches="):
- depswb = line.split("=")[1].split(",")
- for wb in depswb:
- wb_name = wb.strip()
- if wb_name:
- repo.requires.add(wb_name)
- FreeCAD.Console.PrintLog(
- f"{repo.display_name} requires FreeCAD Addon '{wb_name}'\n"
- )
-
- elif line.startswith("pylibs="):
- depspy = line.split("=")[1].split(",")
- for pl in depspy:
- dep = pl.strip()
- if dep:
- repo.python_requires.add(dep)
- FreeCAD.Console.PrintLog(
- f"{repo.display_name} requires python package '{dep}'\n"
- )
-
- elif line.startswith("optionalpylibs="):
- opspy = line.split("=")[1].split(",")
- for pl in opspy:
- dep = pl.strip()
- if dep:
- repo.python_optional.add(dep)
- FreeCAD.Console.PrintLog(
- f"{repo.display_name} optionally imports python package"
- + f" '{pl.strip()}'\n"
- )
-
- def process_requirements_txt(self, repo: Addon, data: QtCore.QByteArray):
- """Process the requirements.txt metadata file"""
-
- f = self._ensure_string(data, repo.name, "requirements.txt")
- lines = f.splitlines()
- for line in lines:
- break_chars = " <>=~!+#"
- package = line
- for n, c in enumerate(line):
- if c in break_chars:
- package = line[:n].strip()
- break
- if package:
- repo.python_requires.add(package)
-
- def process_icon(self, repo: Addon, data: QtCore.QByteArray):
- """Convert icon data into a valid icon file and store it"""
- cache_file = repo.get_cached_icon_filename()
- with open(cache_file, "wb") as icon_file:
- icon_file.write(data.data())
- repo.cached_icon_filename = cache_file
-
-
-# @}
diff --git a/src/Mod/AddonManager/addonmanager_workers_startup.py b/src/Mod/AddonManager/addonmanager_workers_startup.py
deleted file mode 100644
index 3b0bf82b10..0000000000
--- a/src/Mod/AddonManager/addonmanager_workers_startup.py
+++ /dev/null
@@ -1,1006 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2023 FreeCAD Project Association *
-# * Copyright (c) 2019 Yorik van Havre *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# * . *
-# * *
-# ***************************************************************************
-
-"""Worker thread classes for Addon Manager startup"""
-import hashlib
-import json
-import os
-import queue
-import re
-import shutil
-import stat
-import threading
-import time
-from typing import List, Optional
-import xml.etree.ElementTree
-
-from PySide import QtCore
-
-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
-import addonmanager_freecad_interface as fci
-
-translate = FreeCAD.Qt.translate
-
-# Workers only have one public method by design
-# pylint: disable=c-extension-no-member,too-few-public-methods,too-many-instance-attributes
-
-
-class CreateAddonListWorker(QtCore.QThread):
- """This worker updates the list of available workbenches, emitting an "addon_repo"
- signal for each Addon as they are processed."""
-
- addon_repo = QtCore.Signal(object)
- progress_made = QtCore.Signal(str, int, int)
-
- def __init__(self):
- QtCore.QThread.__init__(self)
-
- # reject_listed addons
- self.macros_reject_list = []
- self.mod_reject_list = []
-
- # These addons will print an additional message informing the user
- self.obsolete = []
-
- # These addons will print an additional message informing the user Python2 only
- self.py2only = []
-
- self.package_names = []
- self.moddir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod")
- self.current_thread = None
-
- self.git_manager = initialize_git()
-
- def run(self):
- "populates the list of addons"
-
- self.current_thread = QtCore.QThread.currentThread()
- try:
- self._get_freecad_addon_repo_data()
- except ConnectionError:
- return
- self._get_custom_addons()
- self._get_official_addons()
- self._retrieve_macros_from_git()
- self._retrieve_macros_from_wiki()
-
- def _get_freecad_addon_repo_data(self):
- # update info lists
- p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(
- "https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/addonflags.json", 5000
- )
- if p:
- p = p.data().decode("utf8")
- j = json.loads(p)
- if "obsolete" in j and "Mod" in j["obsolete"]:
- self.obsolete = j["obsolete"]["Mod"]
-
- if "blacklisted" in j and "Macro" in j["blacklisted"]:
- self.macros_reject_list = j["blacklisted"]["Macro"]
-
- if "blacklisted" in j and "Mod" in j["blacklisted"]:
- self.mod_reject_list = j["blacklisted"]["Mod"]
-
- if "py2only" in j and "Mod" in j["py2only"]:
- self.py2only = j["py2only"]["Mod"]
-
- if "deprecated" in j:
- self._process_deprecated(j["deprecated"])
-
- else:
- message = translate(
- "AddonsInstaller",
- "Failed to connect to GitHub. Check your connection and proxy settings.",
- )
- FreeCAD.Console.PrintError(message + "\n")
- raise ConnectionError
-
- def _process_deprecated(self, deprecated_addons):
- """Parse the section on deprecated addons"""
-
- fc_major = int(FreeCAD.Version()[0])
- fc_minor = int(FreeCAD.Version()[1])
- for item in deprecated_addons:
- if "as_of" in item and "name" in item:
- try:
- version_components = item["as_of"].split(".")
- major = int(version_components[0])
- if len(version_components) > 1:
- minor = int(version_components[1])
- else:
- minor = 0
- if major < fc_major or (major == fc_major and minor <= fc_minor):
- if "kind" not in item or item["kind"] == "mod":
- self.obsolete.append(item["name"])
- elif item["kind"] == "macro":
- self.macros_reject_list.append(item["name"])
- else:
- FreeCAD.Console.PrintMessage(
- f'Unrecognized Addon kind {item["kind"]} in deprecation list.'
- )
- except ValueError:
- FreeCAD.Console.PrintMessage(
- f"Failed to parse version from {item['name']}, version {item['as_of']}"
- )
-
- def _get_custom_addons(self):
-
- # querying custom addons first
- addon_list = (
- FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
- .GetString("CustomRepositories", "")
- .split("\n")
- )
- custom_addons = []
- for addon in addon_list:
- if " " in addon:
- addon_and_branch = addon.split(" ")
- custom_addons.append({"url": addon_and_branch[0], "branch": addon_and_branch[1]})
- else:
- custom_addons.append({"url": addon, "branch": "master"})
- for addon in custom_addons:
- if self.current_thread.isInterruptionRequested():
- return
- if addon and addon["url"]:
- if addon["url"][-1] == "/":
- addon["url"] = addon["url"][0:-1] # Strip trailing slash
- addon["url"] = addon["url"].split(".git")[0] # Remove .git
- name = addon["url"].split("/")[-1]
- if name in self.package_names:
- # We already have something with this name, skip this one
- FreeCAD.Console.PrintWarning(
- translate("AddonsInstaller", "WARNING: Duplicate addon {} ignored").format(
- name
- )
- )
- continue
- FreeCAD.Console.PrintLog(
- f"Adding custom location {addon['url']} with branch {addon['branch']}\n"
- )
- self.package_names.append(name)
- addondir = os.path.join(self.moddir, name)
- if os.path.exists(addondir) and os.listdir(addondir):
- state = Addon.Status.UNCHECKED
- else:
- state = Addon.Status.NOT_INSTALLED
- repo = Addon(name, addon["url"], state, addon["branch"])
- md_file = os.path.join(addondir, "package.xml")
- if os.path.isfile(md_file):
- try:
- repo.installed_metadata = MetadataReader.from_file(md_file)
- repo.installed_version = repo.installed_metadata.version
- repo.updated_timestamp = os.path.getmtime(md_file)
- repo.verify_url_and_branch(addon["url"], addon["branch"])
- except xml.etree.ElementTree.ParseError:
- fci.Console.PrintWarning(
- "An invalid or corrupted package.xml file was installed for"
- )
- fci.Console.PrintWarning(
- f" custom addon {self.name}... ignoring the bad data.\n"
- )
-
- self.addon_repo.emit(repo)
-
- def _get_official_addons(self):
- # querying official addons
- p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(
- "https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/.gitmodules", 5000
- )
- if not p:
- return
- p = p.data().decode("utf8")
- p = re.findall(
- (
- r'(?m)\[submodule\s*"(?P.*)"\]\s*'
- r"path\s*=\s*(?P.+)\s*"
- r"url\s*=\s*(?Phttps?://.*)\s*"
- r"(branch\s*=\s*(?P[^\s]*)\s*)?"
- ),
- p,
- )
- for name, _, url, _, branch in p:
- if self.current_thread.isInterruptionRequested():
- return
- if name in self.package_names:
- # We already have something with this name, skip this one
- continue
- self.package_names.append(name)
- if branch is None or len(branch) == 0:
- branch = "master"
- url = url.split(".git")[0]
- addondir = os.path.join(self.moddir, name)
- if os.path.exists(addondir) and os.listdir(addondir):
- # make sure the folder exists and it contains files!
- state = Addon.Status.UNCHECKED
- else:
- state = Addon.Status.NOT_INSTALLED
- repo = Addon(name, url, state, branch)
- md_file = os.path.join(addondir, "package.xml")
- if os.path.isfile(md_file):
- try:
- repo.installed_metadata = MetadataReader.from_file(md_file)
- repo.installed_version = repo.installed_metadata.version
- repo.updated_timestamp = os.path.getmtime(md_file)
- repo.verify_url_and_branch(url, branch)
- except xml.etree.ElementTree.ParseError:
- fci.Console.PrintWarning(
- "An invalid or corrupted package.xml file was installed for"
- )
- fci.Console.PrintWarning(f" addon {self.name}... ignoring the bad data.\n")
-
- if name in self.py2only:
- repo.python2 = True
- if name in self.mod_reject_list:
- repo.rejected = True
- if name in self.obsolete:
- repo.obsolete = True
- self.addon_repo.emit(repo)
-
- def _retrieve_macros_from_git(self):
- """Retrieve macros from FreeCAD-macros.git
-
- Emits a signal for each macro in
- https://github.com/FreeCAD/FreeCAD-macros.git
- """
-
- macro_cache_location = utils.get_cache_file_name("Macros")
-
- if not self.git_manager:
- message = translate(
- "AddonsInstaller",
- "Git is disabled, skipping Git macros",
- )
- FreeCAD.Console.PrintWarning(message + "\n")
- return
-
- update_succeeded = self._update_local_git_repo()
- if not update_succeeded:
- return
-
- n_files = 0
- for _, _, filenames in os.walk(macro_cache_location):
- n_files += len(filenames)
- counter = 0
- for dirpath, _, filenames in os.walk(macro_cache_location):
- counter += 1
- if self.current_thread.isInterruptionRequested():
- return
- if ".git" in dirpath:
- continue
- for filename in filenames:
- if self.current_thread.isInterruptionRequested():
- return
- if filename.lower().endswith(".fcmacro"):
- macro = Macro(filename[:-8]) # Remove ".FCMacro".
- if macro.name in self.package_names:
- FreeCAD.Console.PrintLog(
- f"Ignoring second macro named {macro.name} (found on git)\n"
- )
- continue # We already have a macro with this name
- self.package_names.append(macro.name)
- macro.on_git = True
- macro.src_filename = os.path.join(dirpath, filename)
- macro.fill_details_from_file(macro.src_filename)
- repo = Addon.from_macro(macro)
- FreeCAD.Console.PrintLog(f"Found macro {repo.name}\n")
- repo.url = "https://github.com/FreeCAD/FreeCAD-macros.git"
- utils.update_macro_installation_details(repo)
- self.addon_repo.emit(repo)
-
- def _update_local_git_repo(self) -> bool:
- macro_cache_location = utils.get_cache_file_name("Macros")
- try:
- if os.path.exists(macro_cache_location):
- if not os.path.exists(os.path.join(macro_cache_location, ".git")):
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Attempting to change non-Git Macro setup to use Git\n",
- )
- )
- self.git_manager.repair(
- "https://github.com/FreeCAD/FreeCAD-macros.git",
- macro_cache_location,
- )
- self.git_manager.update(macro_cache_location)
- else:
- self.git_manager.clone(
- "https://github.com/FreeCAD/FreeCAD-macros.git",
- macro_cache_location,
- )
- except GitFailed as e:
- FreeCAD.Console.PrintMessage(
- translate(
- "AddonsInstaller",
- "An error occurred updating macros from GitHub, trying clean checkout...",
- )
- + f":\n{e}\n"
- )
- FreeCAD.Console.PrintMessage(f"{macro_cache_location}\n")
- FreeCAD.Console.PrintMessage(
- translate("AddonsInstaller", "Attempting to do a clean checkout...") + "\n"
- )
- try:
- os.chdir(
- os.path.join(macro_cache_location, "..")
- ) # Make sure we are not IN this directory
- shutil.rmtree(macro_cache_location, onerror=self._remove_readonly)
- self.git_manager.clone(
- "https://github.com/FreeCAD/FreeCAD-macros.git",
- macro_cache_location,
- )
- FreeCAD.Console.PrintMessage(
- translate("AddonsInstaller", "Clean checkout succeeded") + "\n"
- )
- except GitFailed as e2:
- # The Qt Python translation extractor doesn't support splitting this string (yet)
- # pylint: disable=line-too-long
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Failed to update macros from GitHub -- try clearing the Addon Manager's cache.",
- )
- + f":\n{str(e2)}\n"
- )
- return False
- return True
-
- def _retrieve_macros_from_wiki(self):
- """Retrieve macros from the wiki
-
- Read the wiki and emit a signal for each found macro.
- Reads only the page https://wiki.freecad.org/Macros_recipes
- """
-
- p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(
- "https://wiki.freecad.org/Macros_recipes", 5000
- )
- if not p:
- # The Qt Python translation extractor doesn't support splitting this string (yet)
- # pylint: disable=line-too-long
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Error connecting to the Wiki, FreeCAD cannot retrieve the Wiki macro list at this time",
- )
- + "\n"
- )
- return
- p = p.data().decode("utf8")
- macros = re.findall(r'title="(Macro.*?)"', p)
- macros = [mac for mac in macros if "translated" not in mac]
- macro_names = []
- for _, mac in enumerate(macros):
- if self.current_thread.isInterruptionRequested():
- return
- macname = mac[6:] # Remove "Macro ".
- macname = macname.replace("&", "&")
- if not macname:
- continue
- if (
- (macname not in self.macros_reject_list)
- and ("recipes" not in macname.lower())
- and (macname not in macro_names)
- ):
- macro_names.append(macname)
- macro = Macro(macname)
- if macro.name in self.package_names:
- FreeCAD.Console.PrintLog(
- f"Ignoring second macro named {macro.name} (found on wiki)\n"
- )
- continue # We already have a macro with this name
- self.package_names.append(macro.name)
- macro.on_wiki = True
- macro.parsed = False
- repo = Addon.from_macro(macro)
- repo.url = "https://wiki.freecad.org/Macros_recipes"
- utils.update_macro_installation_details(repo)
- self.addon_repo.emit(repo)
-
- def _remove_readonly(self, func, path, _) -> None:
- """Remove a read-only file."""
-
- os.chmod(path, stat.S_IWRITE)
- func(path)
-
-
-class LoadPackagesFromCacheWorker(QtCore.QThread):
- """A subthread worker that loads package information from its cache file."""
-
- addon_repo = QtCore.Signal(object)
-
- def __init__(self, cache_file: str):
- QtCore.QThread.__init__(self)
- self.cache_file = cache_file
- self.metadata_cache_path = os.path.join(
- FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata"
- )
-
- def override_metadata_cache_path(self, path):
- """For testing purposes, override the location to fetch the package metadata from."""
- self.metadata_cache_path = path
-
- def run(self):
- """Rarely called directly: create an instance and call start() on it instead to
- launch in a new thread"""
- with open(self.cache_file, encoding="utf-8") as f:
- data = f.read()
- if data:
- dict_data = json.loads(data)
- for item in dict_data.values():
- if QtCore.QThread.currentThread().isInterruptionRequested():
- return
- repo = Addon.from_cache(item)
- repo_metadata_cache_path = os.path.join(
- self.metadata_cache_path, repo.name, "package.xml"
- )
- if os.path.isfile(repo_metadata_cache_path):
- try:
- repo.load_metadata_file(repo_metadata_cache_path)
- except RuntimeError as e:
- FreeCAD.Console.PrintLog(f"Failed loading {repo_metadata_cache_path}\n")
- FreeCAD.Console.PrintLog(str(e) + "\n")
- self.addon_repo.emit(repo)
-
-
-class LoadMacrosFromCacheWorker(QtCore.QThread):
- """A worker object to load macros from a cache file"""
-
- add_macro_signal = QtCore.Signal(object)
-
- def __init__(self, cache_file: str):
- QtCore.QThread.__init__(self)
- self.cache_file = cache_file
-
- def run(self):
- """Rarely called directly: create an instance and call start() on it instead to
- launch in a new thread"""
-
- with open(self.cache_file, encoding="utf-8") as f:
- data = f.read()
- dict_data = json.loads(data)
- for item in dict_data:
- if QtCore.QThread.currentThread().isInterruptionRequested():
- return
- new_macro = Macro.from_cache(item)
- repo = Addon.from_macro(new_macro)
- utils.update_macro_installation_details(repo)
- self.add_macro_signal.emit(repo)
-
-
-class CheckSingleUpdateWorker(QtCore.QObject):
- """This worker is a little different from the others: the actual recommended way of
- running in a QThread is to make a worker object that gets moved into the thread."""
-
- update_status = QtCore.Signal(int)
-
- def __init__(self, repo: Addon, parent: QtCore.QObject = None):
- super().__init__(parent)
- self.repo = repo
-
- def do_work(self):
- """Use the UpdateChecker class to do the work of this function, depending on the
- type of Addon"""
-
- checker = UpdateChecker()
- if self.repo.repo_type == Addon.Kind.WORKBENCH:
- checker.check_workbench(self.repo)
- elif self.repo.repo_type == Addon.Kind.MACRO:
- checker.check_macro(self.repo)
- elif self.repo.repo_type == Addon.Kind.PACKAGE:
- checker.check_package(self.repo)
-
- self.update_status.emit(self.repo.update_status)
-
-
-class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
- """This worker checks for available updates for all workbenches"""
-
- update_status = QtCore.Signal(Addon)
- progress_made = QtCore.Signal(str, int, int)
-
- def __init__(self, repos: List[Addon]):
-
- QtCore.QThread.__init__(self)
- self.repos = repos
- self.current_thread = None
- self.basedir = FreeCAD.getUserAppDataDir()
- self.moddir = os.path.join(self.basedir, "Mod")
-
- def run(self):
- """Rarely called directly: create an instance and call start() on it instead to
- launch in a new thread"""
-
- self.current_thread = QtCore.QThread.currentThread()
- checker = UpdateChecker()
- count = 1
- for repo in self.repos:
- if self.current_thread.isInterruptionRequested():
- return
- message = translate("AddonsInstaller", "Checking {} for update").format(
- repo.display_name
- )
- self.progress_made.emit(message, count, len(self.repos))
- count += 1
- if repo.status() == Addon.Status.UNCHECKED:
- if repo.repo_type == Addon.Kind.WORKBENCH:
- checker.check_workbench(repo)
- self.update_status.emit(repo)
- elif repo.repo_type == Addon.Kind.MACRO:
- checker.check_macro(repo)
- self.update_status.emit(repo)
- elif repo.repo_type == Addon.Kind.PACKAGE:
- checker.check_package(repo)
- self.update_status.emit(repo)
-
-
-class UpdateChecker:
- """A utility class used by the CheckWorkbenchesForUpdatesWorker class. Each function is
- designed for a specific Addon type, and modifies the passed-in Addon with the determined
- update status."""
-
- def __init__(self):
- self.basedir = FreeCAD.getUserAppDataDir()
- self.moddir = os.path.join(self.basedir, "Mod")
- self.git_manager = initialize_git()
-
- def override_mod_directory(self, moddir):
- """Primarily for use when testing, sets an alternate directory to use for mods"""
- self.moddir = moddir
-
- def check_workbench(self, wb):
- """Given a workbench Addon wb, check it for updates using git. If git is not
- available, does nothing."""
- if not self.git_manager:
- wb.set_status(Addon.Status.CANNOT_CHECK)
- return
- clonedir = os.path.join(self.moddir, wb.name)
- if os.path.exists(clonedir):
- # mark as already installed AND already checked for updates
- if not os.path.exists(os.path.join(clonedir, ".git")):
- with wb.git_lock:
- self.git_manager.repair(wb.url, clonedir)
- with wb.git_lock:
- try:
- status = self.git_manager.status(clonedir)
- if "(no branch)" in status:
- # By definition, in a detached-head state we cannot
- # update, so don't even bother checking.
- wb.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
- wb.branch = self.git_manager.current_branch(clonedir)
- return
- except GitFailed as e:
- FreeCAD.Console.PrintWarning(
- "AddonManager: "
- + translate(
- "AddonsInstaller",
- "Unable to fetch Git updates for workbench {}",
- ).format(wb.name)
- + "\n"
- )
- FreeCAD.Console.PrintWarning(str(e) + "\n")
- wb.set_status(Addon.Status.CANNOT_CHECK)
- else:
- try:
- if self.git_manager.update_available(clonedir):
- wb.set_status(Addon.Status.UPDATE_AVAILABLE)
- else:
- wb.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
- except GitFailed:
- FreeCAD.Console.PrintWarning(
- translate("AddonsInstaller", "Git status failed for {}").format(wb.name)
- + "\n"
- )
- wb.set_status(Addon.Status.CANNOT_CHECK)
-
- def _branch_name_changed(self, package: Addon) -> bool:
- clone_dir = os.path.join(self.moddir, package.name)
- installed_metadata_file = os.path.join(clone_dir, "package.xml")
- if not os.path.isfile(installed_metadata_file):
- return False
- if not hasattr(package, "metadata") or package.metadata is None:
- return False
- try:
- installed_metadata = MetadataReader.from_file(installed_metadata_file)
- installed_default_branch = get_branch_from_metadata(installed_metadata)
- remote_default_branch = get_branch_from_metadata(package.metadata)
- if installed_default_branch != remote_default_branch:
- return True
- except RuntimeError:
- return False
- return False
-
- def check_package(self, package: Addon) -> None:
- """Given a packaged Addon package, check it for updates. If git is available that is
- used. If not, the package's metadata is examined, and if the metadata file has changed
- compared to the installed copy, an update is flagged. In addition, a change to the
- default branch name triggers an update."""
-
- clone_dir = self.moddir + os.sep + package.name
- if os.path.exists(clone_dir):
-
- # First, see if the branch name changed, which automatically triggers an update
- if self._branch_name_changed(package):
- package.set_status(Addon.Status.UPDATE_AVAILABLE)
- return
-
- # Next, try to just do a git-based update, which will give the most accurate results:
- if self.git_manager:
- self.check_workbench(package)
- if package.status() != Addon.Status.CANNOT_CHECK:
- # It worked, just exit now
- return
-
- # If we were unable to do a git-based update, try using the package.xml file instead:
- installed_metadata_file = os.path.join(clone_dir, "package.xml")
- if not os.path.isfile(installed_metadata_file):
- # If there is no package.xml file, then it's because the package author added it
- # after the last time the local installation was updated. By definition, then,
- # there is an update available, if only to download the new XML file.
- package.set_status(Addon.Status.UPDATE_AVAILABLE)
- package.installed_version = None
- return
- package.updated_timestamp = os.path.getmtime(installed_metadata_file)
- try:
- installed_metadata = MetadataReader.from_file(installed_metadata_file)
- package.installed_version = installed_metadata.version
- # Packages are considered up-to-date if the metadata version matches.
- # Authors should update their version string when they want the addon
- # manager to alert users of a new version.
- if package.metadata.version != installed_metadata.version:
- package.set_status(Addon.Status.UPDATE_AVAILABLE)
- else:
- package.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
- except RuntimeError:
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Failed to read metadata from {name}",
- ).format(name=installed_metadata_file)
- + "\n"
- )
- package.set_status(Addon.Status.CANNOT_CHECK)
-
- def check_macro(self, macro_wrapper: Addon) -> None:
- """Check to see if the online copy of the macro's code differs from the local copy."""
-
- # Make sure this macro has its code downloaded:
- try:
- if not macro_wrapper.macro.parsed and macro_wrapper.macro.on_git:
- macro_wrapper.macro.fill_details_from_file(macro_wrapper.macro.src_filename)
- elif not macro_wrapper.macro.parsed and macro_wrapper.macro.on_wiki:
- mac = macro_wrapper.macro.name.replace(" ", "_")
- mac = mac.replace("&", "%26")
- mac = mac.replace("+", "%2B")
- url = "https://wiki.freecad.org/Macro_" + mac
- macro_wrapper.macro.fill_details_from_wiki(url)
- except RuntimeError:
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Failed to fetch code for macro '{name}'",
- ).format(name=macro_wrapper.macro.name)
- + "\n"
- )
- macro_wrapper.set_status(Addon.Status.CANNOT_CHECK)
- return
-
- hasher1 = hashlib.sha1()
- hasher2 = hashlib.sha1()
- hasher1.update(macro_wrapper.macro.code.encode("utf-8"))
- new_sha1 = hasher1.hexdigest()
- test_file_one = os.path.join(FreeCAD.getUserMacroDir(True), macro_wrapper.macro.filename)
- test_file_two = os.path.join(
- FreeCAD.getUserMacroDir(True), "Macro_" + macro_wrapper.macro.filename
- )
- if os.path.exists(test_file_one):
- with open(test_file_one, "rb") as f:
- contents = f.read()
- hasher2.update(contents)
- old_sha1 = hasher2.hexdigest()
- elif os.path.exists(test_file_two):
- with open(test_file_two, "rb") as f:
- contents = f.read()
- hasher2.update(contents)
- old_sha1 = hasher2.hexdigest()
- else:
- return
- if new_sha1 == old_sha1:
- macro_wrapper.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
- else:
- macro_wrapper.set_status(Addon.Status.UPDATE_AVAILABLE)
-
-
-class CacheMacroCodeWorker(QtCore.QThread):
- """Download and cache the macro code, and parse its internal metadata"""
-
- update_macro = QtCore.Signal(Addon)
- progress_made = QtCore.Signal(str, int, int)
-
- def __init__(self, repos: List[Addon]) -> None:
- QtCore.QThread.__init__(self)
- self.repos = repos
- self.workers = []
- self.terminators = []
- self.lock = threading.Lock()
- self.failed = []
- self.counter = 0
- self.repo_queue = None
-
- def run(self):
- """Rarely called directly: create an instance and call start() on it instead to
- launch in a new thread"""
-
- self.repo_queue = queue.Queue()
- num_macros = 0
- for repo in self.repos:
- if repo.macro is not None:
- self.repo_queue.put(repo)
- num_macros += 1
-
- interrupted = self._process_queue(num_macros)
- if interrupted:
- return
-
- # Make sure all of our child threads have fully exited:
- for worker in self.workers:
- worker.wait(50)
- if not worker.isFinished():
- # The Qt Python translation extractor doesn't support splitting this string (yet)
- # pylint: disable=line-too-long
- FreeCAD.Console.PrintError(
- translate(
- "AddonsInstaller",
- "Addon Manager: a worker process failed to complete while fetching {name}",
- ).format(name=worker.macro.name)
- + "\n"
- )
-
- self.repo_queue.join()
- for terminator in self.terminators:
- if terminator and terminator.isActive():
- terminator.stop()
-
- if len(self.failed) > 0:
- num_failed = len(self.failed)
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Out of {num_macros} macros, {num_failed} timed out while processing",
- ).format(num_macros=num_macros, num_failed=num_failed)
- + "\n"
- )
-
- def _process_queue(self, num_macros) -> bool:
- """Spools up six network connections and downloads the macro code. Returns True if
- it was interrupted by user request, or False if it ran to completion."""
-
- # Emulate QNetworkAccessManager and spool up six connections:
- for _ in range(6):
- self.update_and_advance(None)
-
- current_thread = QtCore.QThread.currentThread()
- while True:
- if current_thread.isInterruptionRequested():
- for worker in self.workers:
- worker.blockSignals(True)
- worker.requestInterruption()
- if not worker.wait(100):
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Addon Manager: a worker process failed to halt ({name})",
- ).format(name=worker.macro.name)
- + "\n"
- )
- return True
- # Ensure our signals propagate out by running an internal thread-local event loop
- QtCore.QCoreApplication.processEvents()
- with self.lock:
- if self.counter >= num_macros:
- break
- time.sleep(0.1)
- return False
-
- def update_and_advance(self, repo: Optional[Addon]) -> None:
- """Emit the updated signal and launch the next item from the queue."""
- if repo is not None:
- if repo.macro.name not in self.failed:
- self.update_macro.emit(repo)
- self.repo_queue.task_done()
- with self.lock:
- self.counter += 1
-
- if QtCore.QThread.currentThread().isInterruptionRequested():
- return
-
- if repo is not None:
- message = translate("AddonsInstaller", "Caching {} macro").format(repo.display_name)
- else:
- message = translate("AddonsInstaller", "Caching macros")
- self.progress_made.emit(message, len(self.repos) - self.repo_queue.qsize(), len(self.repos))
-
- try:
- next_repo = self.repo_queue.get_nowait()
- worker = GetMacroDetailsWorker(next_repo)
- worker.finished.connect(lambda: self.update_and_advance(next_repo))
- with self.lock:
- self.workers.append(worker)
- self.terminators.append(
- QtCore.QTimer.singleShot(10000, lambda: self.terminate(worker))
- )
- worker.start()
- except queue.Empty:
- pass
-
- def terminate(self, worker) -> None:
- """Shut down all running workers and exit the thread"""
- if not worker.isFinished():
- macro_name = worker.macro.name
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Timeout while fetching metadata for macro {}",
- ).format(macro_name)
- + "\n"
- )
- # worker.blockSignals(True)
- worker.requestInterruption()
- worker.wait(100)
- if worker.isRunning():
- FreeCAD.Console.PrintError(
- translate(
- "AddonsInstaller",
- "Failed to kill process for macro {}!\n",
- ).format(macro_name)
- )
- with self.lock:
- self.failed.append(macro_name)
-
-
-class GetMacroDetailsWorker(QtCore.QThread):
- """Retrieve the macro details for a macro"""
-
- readme_updated = QtCore.Signal(str)
-
- def __init__(self, repo):
-
- QtCore.QThread.__init__(self)
- self.macro = repo.macro
-
- def run(self):
- """Rarely called directly: create an instance and call start() on it instead to
- launch in a new thread"""
-
- if not self.macro.parsed and self.macro.on_git:
- self.macro.fill_details_from_file(self.macro.src_filename)
- if not self.macro.parsed and self.macro.on_wiki:
- mac = self.macro.name.replace(" ", "_")
- mac = mac.replace("&", "%26")
- mac = mac.replace("+", "%2B")
- url = "https://wiki.freecad.org/Macro_" + mac
- self.macro.fill_details_from_wiki(url)
- message = (
- ""
- + self.macro.name
- + "
"
- + self.macro.desc
- + '
Macro location: '
- + self.macro.url
- + ""
- )
- 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)
-
-
-class GetAddonScoreWorker(QtCore.QThread):
- """Fetch data from an addon score file."""
-
- update_addon_score = 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"""
-
- if self.url != "TEST":
- 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 score from '{}' -- sorting by score will fail\n",
- ).format(self.url)
- )
- return
- text_result = fetch_result.data().decode("utf8")
- json_result = json.loads(text_result)
- else:
- FreeCAD.Console.PrintWarning("Running score generation in TEST mode...\n")
- json_result = {}
- for addon in self.addons:
- if addon.macro:
- json_result[addon.name] = len(addon.macro.comment) if addon.macro.comment else 0
- else:
- json_result[addon.url] = len(addon.description) if addon.description else 0
-
- for addon in self.addons:
- score = None
- if addon.url in json_result:
- score = json_result[addon.url]
- elif addon.name in json_result:
- score = json_result[addon.name]
- if score is not None:
- try:
- addon.score = int(score)
- self.update_addon_score.emit(addon)
- except (ValueError, OverflowError):
- FreeCAD.Console.PrintLog(
- f"Failed to convert score value '{score}' to an integer for {addon.name}"
- )
diff --git a/src/Mod/AddonManager/addonmanager_workers_utility.py b/src/Mod/AddonManager/addonmanager_workers_utility.py
deleted file mode 100644
index 5c8aa1b538..0000000000
--- a/src/Mod/AddonManager/addonmanager_workers_utility.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2023 FreeCAD Project Association *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# * . *
-# * *
-# ***************************************************************************
-
-"""Misc. worker thread classes for the FreeCAD Addon Manager."""
-
-from typing import Optional
-
-import FreeCAD
-from PySide import QtCore
-import NetworkManager
-import time
-
-translate = FreeCAD.Qt.translate
-
-
-class ConnectionChecker(QtCore.QThread):
- """A worker thread for checking the connection to GitHub as a proxy for overall
- network connectivity. It has two signals: success() and failure(str). The failure
- signal contains a translated error message suitable for display to an end user."""
-
- success = QtCore.Signal()
- failure = QtCore.Signal(str)
-
- def __init__(self):
- QtCore.QThread.__init__(self)
- self.done = False
- self.request_id = None
- self.data = None
-
- def run(self):
- """Not generally called directly: create a new ConnectionChecker object and call start()
- on it to spawn a child thread."""
-
- FreeCAD.Console.PrintLog("Checking network connection...\n")
- url = "https://api.github.com/zen"
- self.done = False
- NetworkManager.AM_NETWORK_MANAGER.completed.connect(self.connection_data_received)
- self.request_id = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
- url, timeout_ms=10000
- )
- while not self.done:
- if QtCore.QThread.currentThread().isInterruptionRequested():
- FreeCAD.Console.PrintLog("Connection check cancelled\n")
- NetworkManager.AM_NETWORK_MANAGER.abort(self.request_id)
- self.disconnect_network_manager()
- return
- QtCore.QCoreApplication.processEvents()
- time.sleep(0.1)
- if not self.data:
- self.failure.emit(
- translate(
- "AddonsInstaller",
- "Unable to read data from GitHub: check your internet connection and proxy "
- "settings and try again.",
- )
- )
- self.disconnect_network_manager()
- return
- FreeCAD.Console.PrintLog(f"GitHub's zen message response: {self.data.decode('utf-8')}\n")
- self.disconnect_network_manager()
- self.success.emit()
-
- def connection_data_received(self, id: int, status: int, data: QtCore.QByteArray):
- if self.request_id is not None and self.request_id == id:
- if status == 200:
- self.data = data.data()
- else:
- FreeCAD.Console.PrintWarning(f"No data received: status returned was {status}\n")
- self.data = None
- self.done = True
-
- def disconnect_network_manager(self):
- NetworkManager.AM_NETWORK_MANAGER.completed.disconnect(self.connection_data_received)
diff --git a/src/Mod/AddonManager/change_branch.py b/src/Mod/AddonManager/change_branch.py
deleted file mode 100644
index 1880ab95be..0000000000
--- a/src/Mod/AddonManager/change_branch.py
+++ /dev/null
@@ -1,310 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2025 The FreeCAD Project Association AISBL *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# * . *
-# * *
-# ***************************************************************************
-
-"""The Change Branch dialog and utility classes and methods"""
-
-import os
-from typing import Dict
-
-import addonmanager_freecad_interface as fci
-import addonmanager_utilities as utils
-
-from addonmanager_git import initialize_git, GitFailed
-
-try:
- from PySide import QtWidgets, QtCore
-except ImportError:
- try:
- from PySide6 import QtWidgets, QtCore
- except ImportError:
- from PySide2 import QtWidgets, QtCore # pylint: disable=deprecated-module
-
-translate = fci.translate
-
-
-class ChangeBranchDialog(QtWidgets.QWidget):
- """A dialog that displays available git branches and allows the user to select one to change
- to. Includes code that does that change, as well as some modal dialogs to warn them of the
- possible consequences and display various error messages."""
-
- branch_changed = QtCore.Signal(str, str)
-
- def __init__(self, path: str, parent=None):
- super().__init__(parent)
-
- self.ui = utils.loadUi(os.path.join(os.path.dirname(__file__), "change_branch.ui"))
-
- self.item_filter = ChangeBranchDialogFilter()
- self.ui.tableView.setModel(self.item_filter)
-
- self.item_model = ChangeBranchDialogModel(path, self)
- self.item_filter.setSourceModel(self.item_model)
- self.ui.tableView.sortByColumn(
- 2, QtCore.Qt.DescendingOrder
- ) # Default to sorting by remote last-changed date
-
- # Figure out what row gets selected:
- git_manager = initialize_git()
- if git_manager is None:
- return
-
- row = 0
- self.current_ref = git_manager.current_branch(path)
- selection_model = self.ui.tableView.selectionModel()
- for ref in self.item_model.branches:
- if ref["ref_name"] == self.current_ref:
- index = self.item_filter.mapFromSource(self.item_model.index(row, 0))
- selection_model.select(index, QtCore.QItemSelectionModel.ClearAndSelect)
- selection_model.select(index.siblingAtColumn(1), QtCore.QItemSelectionModel.Select)
- selection_model.select(index.siblingAtColumn(2), QtCore.QItemSelectionModel.Select)
- break
- row += 1
-
- # Make sure the column widths are OK:
- header = self.ui.tableView.horizontalHeader()
- header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
-
- def exec(self):
- """Run the Change Branch dialog and its various sub-dialogs. May result in the branch
- being changed. Code that cares if that happens should connect to the branch_changed
- signal."""
- if self.ui.exec() == QtWidgets.QDialog.Accepted:
-
- selection = self.ui.tableView.selectedIndexes()
- index = self.item_filter.mapToSource(selection[0])
- ref = self.item_model.data(index, ChangeBranchDialogModel.RefAccessRole)
-
- if ref["ref_name"] == self.item_model.current_branch:
- # This is the one we are already on... just return
- return
-
- result = QtWidgets.QMessageBox.critical(
- self,
- translate("AddonsInstaller", "DANGER: Developer feature"),
- translate(
- "AddonsInstaller",
- "DANGER: Switching branches is intended for developers and beta testers, "
- "and may result in broken, non-backwards compatible documents, instability, "
- "crashes, and/or the premature heat death of the universe. Are you sure you "
- "want to continue?",
- ),
- QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel,
- QtWidgets.QMessageBox.Cancel,
- )
- if result == QtWidgets.QMessageBox.Cancel:
- return
- if self.item_model.dirty:
- result = QtWidgets.QMessageBox.critical(
- self,
- translate("AddonsInstaller", "There are local changes"),
- translate(
- "AddonsInstaller",
- "WARNING: This repo has uncommitted local changes. Are you sure you want "
- "to change branches (bringing the changes with you)?",
- ),
- QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel,
- QtWidgets.QMessageBox.Cancel,
- )
- if result == QtWidgets.QMessageBox.Cancel:
- return
-
- self.change_branch(self.item_model.path, ref)
-
- def change_branch(self, path: str, ref: Dict[str, str]) -> None:
- """Change the git clone in `path` to git ref `ref`. Emits the branch_changed signal
- on success."""
- remote_name = ref["ref_name"]
- _, _, local_name = ref["ref_name"].rpartition("/")
- gm = initialize_git()
- if gm is None:
- self._show_no_git_dialog()
- return
-
- try:
- if ref["upstream"]:
- gm.checkout(path, remote_name)
- else:
- gm.checkout(path, remote_name, args=["-b", local_name])
- self.branch_changed.emit(self.current_ref, local_name)
- except GitFailed:
- self._show_git_failed_dialog()
-
- def _show_no_git_dialog(self):
- QtWidgets.QMessageBox.critical(
- self,
- translate("AddonsInstaller", "Cannot find git"),
- translate(
- "AddonsInstaller",
- "Could not find git executable: cannot change branch",
- ),
- QtWidgets.QMessageBox.Ok,
- QtWidgets.QMessageBox.Ok,
- )
-
- def _show_git_failed_dialog(self):
- QtWidgets.QMessageBox.critical(
- self,
- translate("AddonsInstaller", "git operation failed"),
- translate(
- "AddonsInstaller",
- "Git returned an error code when attempting to change branch. There may be "
- "more details in the Report View.",
- ),
- QtWidgets.QMessageBox.Ok,
- QtWidgets.QMessageBox.Ok,
- )
-
-
-class ChangeBranchDialogModel(QtCore.QAbstractTableModel):
- """The data for the dialog comes from git: this model handles the git interactions and
- returns branch information as its rows. Use user data in the RefAccessRole to get information
- about the git refs. RefAccessRole data is a dictionary defined by the GitManager class as the
- results of a `get_branches_with_info()` call."""
-
- branches = []
- DataSortRole = QtCore.Qt.UserRole
- RefAccessRole = QtCore.Qt.UserRole + 1
-
- def __init__(self, path: str, parent=None) -> None:
- super().__init__(parent)
-
- gm = initialize_git()
- self.path = path
- self.branches = gm.get_branches_with_info(path)
- self.current_branch = gm.current_branch(path)
- self.dirty = gm.dirty(path)
- self._remove_tracking_duplicates()
-
- def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
- """Returns the number of rows in the model, e.g. the number of branches."""
- if parent.isValid():
- return 0
- return len(self.branches)
-
- def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
- """Returns the number of columns in the model, e.g. the number of entries in the git ref
- structure (currently 3, 'ref_name', 'upstream', and 'date')."""
- if parent.isValid():
- return 0
- return 3 # Local name, remote name, date
-
- def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole):
- """The data access method for this model. Supports four roles: ToolTipRole, DisplayRole,
- DataSortRole, and RefAccessRole."""
- if not index.isValid():
- return None
- row = index.row()
- column = index.column()
- if role == QtCore.Qt.ToolTipRole:
- return self.branches[row]["author"] + ": " + self.branches[row]["subject"]
- if role == QtCore.Qt.DisplayRole:
- return self._data_display_role(column, row)
- if role == ChangeBranchDialogModel.DataSortRole:
- return self._data_sort_role(column, row)
- if role == ChangeBranchDialogModel.RefAccessRole:
- return self.branches[row]
- return None
-
- def _data_display_role(self, column, row):
- dd = self.branches[row]
- if column == 2:
- if dd["date"] is not None:
- q_date = QtCore.QDateTime.fromString(dd["date"], QtCore.Qt.DateFormat.RFC2822Date)
- return QtCore.QLocale().toString(q_date, QtCore.QLocale.ShortFormat)
- return None
- if column == 0:
- return dd["ref_name"]
- if column == 1:
- return dd["upstream"]
- return None
-
- def _data_sort_role(self, column, row):
- if column == 2:
- if self.branches[row]["date"] is not None:
- q_date = QtCore.QDateTime.fromString(
- self.branches[row]["date"], QtCore.Qt.DateFormat.RFC2822Date
- )
- return q_date
- return None
- if column == 0:
- return self.branches[row]["ref_name"]
- if column == 1:
- return self.branches[row]["upstream"]
- return None
-
- def headerData(
- self,
- section: int,
- orientation: QtCore.Qt.Orientation,
- role: int = QtCore.Qt.DisplayRole,
- ):
- """Returns the header information for the data in this model."""
- if orientation == QtCore.Qt.Vertical:
- return None
- if role != QtCore.Qt.DisplayRole:
- return None
- if section == 0:
- return translate(
- "AddonsInstaller",
- "Local",
- "Table header for local git ref name",
- )
- if section == 1:
- return translate(
- "AddonsInstaller",
- "Remote tracking",
- "Table header for git remote tracking branch name",
- )
- if section == 2:
- return translate(
- "AddonsInstaller",
- "Last Updated",
- "Table header for git update date",
- )
- return None
-
- def _remove_tracking_duplicates(self):
- remote_tracking_branches = []
- branches_to_keep = []
- for branch in self.branches:
- if branch["upstream"]:
- remote_tracking_branches.append(branch["upstream"])
- for branch in self.branches:
- if (
- "HEAD" not in branch["ref_name"]
- and branch["ref_name"] not in remote_tracking_branches
- ):
- branches_to_keep.append(branch)
- self.branches = branches_to_keep
-
-
-class ChangeBranchDialogFilter(QtCore.QSortFilterProxyModel):
- """Uses the DataSortRole in the model to provide a comparison method to sort the data."""
-
- def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex):
- """Compare two git refs according to the DataSortRole in the model."""
- left_data = self.sourceModel().data(left, ChangeBranchDialogModel.DataSortRole)
- right_data = self.sourceModel().data(right, ChangeBranchDialogModel.DataSortRole)
- if left_data is None or right_data is None:
- return right_data is not None
- return left_data < right_data
diff --git a/src/Mod/AddonManager/change_branch.ui b/src/Mod/AddonManager/change_branch.ui
deleted file mode 100644
index 5336eccaed..0000000000
--- a/src/Mod/AddonManager/change_branch.ui
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
- change_branch
-
-
-
- 0
- 0
- 550
- 300
-
-
-
- Change Branch
-
-
- true
-
-
- -
-
-
- Change to branch:
-
-
- true
-
-
-
- -
-
-
- QAbstractItemView::NoEditTriggers
-
-
- false
-
-
- true
-
-
- QAbstractItemView::SingleSelection
-
-
- QAbstractItemView::SelectRows
-
-
- true
-
-
- true
-
-
- true
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- change_branch
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- change_branch
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/compact_view.py b/src/Mod/AddonManager/compact_view.py
deleted file mode 100644
index d92e6c4b8a..0000000000
--- a/src/Mod/AddonManager/compact_view.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# -*- coding: utf-8 -*-
-
-################################################################################
-## Form generated from reading UI file 'compact_view.ui'
-##
-## Created by: Qt User Interface Compiler version 5.15.1
-##
-## WARNING! All changes made in this file will be lost when recompiling UI file!
-################################################################################
-
-from PySide.QtCore import *
-from PySide.QtGui import *
-from PySide.QtWidgets import *
-
-
-class Ui_CompactView(object):
- def setupUi(self, CompactView):
- if not CompactView.objectName():
- CompactView.setObjectName("CompactView")
- CompactView.resize(489, 16)
- sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(CompactView.sizePolicy().hasHeightForWidth())
- CompactView.setSizePolicy(sizePolicy)
- self.horizontalLayout_2 = QHBoxLayout(CompactView)
- self.horizontalLayout_2.setObjectName("horizontalLayout_2")
- self.horizontalLayout_2.setSizeConstraint(QLayout.SetNoConstraint)
- self.horizontalLayout_2.setContentsMargins(3, 0, 9, 0)
- self.labelIcon = QLabel(CompactView)
- self.labelIcon.setObjectName("labelIcon")
- sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
- sizePolicy1.setHorizontalStretch(0)
- sizePolicy1.setVerticalStretch(0)
- sizePolicy1.setHeightForWidth(self.labelIcon.sizePolicy().hasHeightForWidth())
- self.labelIcon.setSizePolicy(sizePolicy1)
- self.labelIcon.setMinimumSize(QSize(16, 16))
- self.labelIcon.setBaseSize(QSize(16, 16))
-
- self.horizontalLayout_2.addWidget(self.labelIcon)
-
- self.labelPackageName = QLabel(CompactView)
- self.labelPackageName.setObjectName("labelPackageName")
-
- self.labelPackageNameSpacer = QLabel(CompactView)
- self.labelPackageNameSpacer.setText(" — ")
- self.labelPackageNameSpacer.setObjectName("labelPackageNameSpacer")
-
- self.horizontalLayout_2.addWidget(self.labelPackageName)
- self.horizontalLayout_2.addWidget(self.labelPackageNameSpacer)
-
- self.labelVersion = QLabel(CompactView)
- self.labelVersion.setObjectName("labelVersion")
-
- self.horizontalLayout_2.addWidget(self.labelVersion)
-
- self.labelDescription = QLabel(CompactView)
- self.labelDescription.setObjectName("labelDescription")
- sizePolicy2 = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
- sizePolicy2.setHorizontalStretch(0)
- sizePolicy2.setVerticalStretch(0)
- sizePolicy2.setHeightForWidth(self.labelDescription.sizePolicy().hasHeightForWidth())
- self.labelDescription.setSizePolicy(sizePolicy2)
- self.labelDescription.setTextFormat(Qt.PlainText)
- self.labelDescription.setWordWrap(False)
- self.labelDescription.setTextInteractionFlags(Qt.TextSelectableByMouse)
-
- self.horizontalLayout_2.addWidget(self.labelDescription)
-
- self.labelStatus = QLabel(CompactView)
- self.labelStatus.setObjectName("labelStatus")
-
- self.horizontalLayout_2.addWidget(self.labelStatus)
-
- self.retranslateUi(CompactView)
-
- QMetaObject.connectSlotsByName(CompactView)
-
- # setupUi
-
- def retranslateUi(self, CompactView):
- # CompactView.setWindowTitle(QCoreApplication.translate("CompactView", "Form", None))
- self.labelIcon.setText(QCoreApplication.translate("CompactView", "Icon", None))
- self.labelPackageName.setText(
- QCoreApplication.translate("CompactView", "Package Name", None)
- )
- self.labelVersion.setText(QCoreApplication.translate("CompactView", "Version", None))
- self.labelDescription.setText(
- QCoreApplication.translate("CompactView", "Description", None)
- )
- self.labelStatus.setText(
- QCoreApplication.translate("CompactView", "Update Available", None)
- )
-
- # retranslateUi
diff --git a/src/Mod/AddonManager/compact_view.ui b/src/Mod/AddonManager/compact_view.ui
deleted file mode 100644
index 604fef6963..0000000000
--- a/src/Mod/AddonManager/compact_view.ui
+++ /dev/null
@@ -1,110 +0,0 @@
-
-
- CompactView
-
-
-
- 0
- 0
- 489
- 16
-
-
-
-
- 0
- 0
-
-
-
- Form
-
-
-
- QLayout::SetNoConstraint
-
-
- 3
-
-
- 0
-
-
- 9
-
-
- 0
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 16
- 16
-
-
-
-
- 16
- 16
-
-
-
- Icon
-
-
-
- -
-
-
- <b>Package Name</b>
-
-
-
- -
-
-
- Version
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Description
-
-
- Qt::PlainText
-
-
- false
-
-
- Qt::TextSelectableByMouse
-
-
-
- -
-
-
- UpdateAvailable
-
-
-
-
-
-
-
-
diff --git a/src/Mod/AddonManager/composite_view.py b/src/Mod/AddonManager/composite_view.py
deleted file mode 100644
index c20a724225..0000000000
--- a/src/Mod/AddonManager/composite_view.py
+++ /dev/null
@@ -1,170 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2024 The FreeCAD Project Association AISBL *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# * . *
-# * *
-# ***************************************************************************
-
-"""Provides a class for showing the list view and detail view at the same time."""
-
-import base64
-
-from addonmanager_freecad_interface import Preferences
-
-from Addon import Addon
-from Widgets.addonmanager_widget_package_details_view import PackageDetailsView
-from Widgets.addonmanager_widget_view_selector import AddonManagerDisplayStyle
-from addonmanager_package_details_controller import PackageDetailsController
-from package_list import PackageList
-
-# Get whatever version of PySide we can
-try:
- import PySide # Use the FreeCAD wrapper
-except ImportError:
- try:
- import PySide6 as PySide # Outside FreeCAD, try Qt6 first
- except ImportError:
- # Fall back to Qt5 (if this fails, Python will kill this module's import)
- import PySide2 as PySide
-
-from PySide import QtCore, QtWidgets
-
-
-class CompositeView(QtWidgets.QWidget):
- """A widget that displays the Addon Manager's top bar, the list of Addons, and the detail
- view. Depending on the view mode selected, these may all be displayed at once, or selecting
- an addon in the list may case the list to hide and the detail view to show."""
-
- install = QtCore.Signal(Addon)
- uninstall = QtCore.Signal(Addon)
- update = QtCore.Signal(Addon)
- execute = QtCore.Signal(Addon)
- update_status = QtCore.Signal(Addon)
- check_for_update = QtCore.Signal(Addon)
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self.package_details = PackageDetailsView(self)
- self.package_details_controller = PackageDetailsController(self.package_details)
- self.package_list = PackageList(self)
- prefs = Preferences()
- self.display_style = prefs.get("ViewStyle")
- self.main_layout = QtWidgets.QHBoxLayout(self)
- self.splitter = QtWidgets.QSplitter(self)
- self.splitter.addWidget(self.package_list)
- self.splitter.addWidget(self.package_details)
- self.splitter.setOrientation(QtCore.Qt.Horizontal)
- self.splitter.setContentsMargins(0, 0, 0, 0)
- self.splitter.setSizePolicy(
- QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
- )
- self.main_layout.addWidget(self.splitter)
- self.layout().setContentsMargins(0, 0, 0, 0)
- self._setup_ui()
- self._setup_connections()
- self._restore_splitter_state()
- self.scroll_position = 0
-
- def _save_splitter_state(self):
- """Write the splitter state into an Addon manager preference, CompositeSplitterState"""
- prefs = Preferences()
- state = self.splitter.saveState()
- encoded = base64.b64encode(state).decode("ASCII")
- prefs.set("CompositeSplitterState", encoded)
-
- def _restore_splitter_state(self):
- """Restore the splitter state from CompositeSplitterState"""
- prefs = Preferences()
- encoded = prefs.get("CompositeSplitterState")
- if encoded:
- state = base64.b64decode(encoded)
- self.splitter.restoreState(state)
-
- def setModel(self, model):
- self.package_list.setModel(model)
-
- def set_display_style(self, style: AddonManagerDisplayStyle):
- self.display_style = style
- self._setup_ui()
-
- def _setup_ui(self):
- if self.display_style == AddonManagerDisplayStyle.EXPANDED:
- self._setup_expanded_ui()
- elif self.display_style == AddonManagerDisplayStyle.COMPACT:
- self._setup_compact_ui()
- elif self.display_style == AddonManagerDisplayStyle.COMPOSITE:
- self._setup_composite_ui()
- else:
- raise RuntimeError("Invalid display style")
- self.package_list.set_view_style(self.display_style)
-
- def _setup_expanded_ui(self):
- self.package_list.show()
- self.package_details.hide()
- self.package_details.button_bar.set_show_back_button(True)
-
- def _setup_compact_ui(self):
- self.package_list.show()
- self.package_details.hide()
- self.package_details.button_bar.set_show_back_button(True)
-
- def _setup_composite_ui(self):
- self.package_list.show()
- self.package_details.show()
- self.package_details.button_bar.set_show_back_button(False)
-
- def _setup_connections(self):
- self.package_list.itemSelected.connect(self.addon_selected)
- self.package_details_controller.back.connect(self._back_button_clicked)
- self.package_details_controller.install.connect(self.install)
- self.package_details_controller.uninstall.connect(self.uninstall)
- self.package_details_controller.update.connect(self.update)
- self.package_details_controller.execute.connect(self.execute)
- self.package_details_controller.update_status.connect(self.update_status)
- self.package_list.ui.view_bar.view_changed.connect(self.set_display_style)
- self.splitter.splitterMoved.connect(self._splitter_moved)
-
- def addon_selected(self, addon):
- """Depending on the display_style, show addon details (possibly hiding the package_list
- widget in the process."""
- self.package_details_controller.show_repo(addon)
- if self.display_style != AddonManagerDisplayStyle.COMPOSITE:
- self.scroll_position = (
- self.package_list.ui.listPackages.verticalScrollBar().sliderPosition()
- )
- print(f"Saved slider position at {self.scroll_position}")
- self.package_list.hide()
- self.package_details.show()
- self.package_details.button_bar.set_show_back_button(True)
-
- def _back_button_clicked(self):
- if self.display_style != AddonManagerDisplayStyle.COMPOSITE:
- print(f"Set slider position to {self.scroll_position}")
- self.package_list.show()
- self.package_details.hide()
- # The following must be done *after* a cycle through the event loop
- QtCore.QTimer.singleShot(
- 0,
- lambda: self.package_list.ui.listPackages.verticalScrollBar().setSliderPosition(
- self.scroll_position
- ),
- )
-
- def _splitter_moved(self, _1: int, _2: int) -> None:
- self._save_splitter_state()
diff --git a/src/Mod/AddonManager/dependency_resolution_dialog.ui b/src/Mod/AddonManager/dependency_resolution_dialog.ui
deleted file mode 100644
index e32ea6427e..0000000000
--- a/src/Mod/AddonManager/dependency_resolution_dialog.ui
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
- DependencyResolutionDialog
-
-
- Qt::ApplicationModal
-
-
-
- 0
- 0
- 455
- 260
-
-
-
- Resolve Dependencies
-
-
- true
-
-
- true
-
-
- -
-
-
- This Addon has the following required and optional dependencies. You must install them before this Addon can be used.
-
-Do you want the Addon Manager to install them automatically? Choose "Ignore" to install the Addon without installing the dependencies.
-
-
- true
-
-
-
- -
-
-
-
-
-
- FreeCAD Addons
-
-
-
-
-
-
-
-
-
- -
-
-
- Required Python modules
-
-
-
-
-
-
-
-
-
- -
-
-
- Optional Python modules
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ignore|QDialogButtonBox::Yes
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- DependencyResolutionDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- DependencyResolutionDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode.ui b/src/Mod/AddonManager/developer_mode.ui
deleted file mode 100644
index 999cca07ce..0000000000
--- a/src/Mod/AddonManager/developer_mode.ui
+++ /dev/null
@@ -1,364 +0,0 @@
-
-
- DeveloperModeDialog
-
-
-
- 0
- 0
- 595
- 677
-
-
-
- Addon Developer Tools
-
-
- true
-
-
- -
-
-
-
-
-
- Path to Addon
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- true
-
-
-
- -
-
-
- Browse...
-
-
-
-
-
- -
-
-
- Metadata
-
-
-
-
-
-
-
-
-
- -
-
-
- Primary branch
-
-
-
- -
-
-
-
-
- -
-
-
- Explanation of what this Addon provides. Displayed in the Addon Manager. It is not necessary for this to state that this is a FreeCAD Addon.
-
-
- Description
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
-
-
-
- -
-
-
- Discussion URL
-
-
-
- -
-
-
- Icon
-
-
-
- -
-
-
- Bugtracker URL
-
-
-
- -
-
-
-
-
-
- Semantic (1.2.3-beta) or CalVer (2022.08.30) styles supported
-
-
-
- -
-
-
- Set to today (CalVer style)
-
-
-
-
-
- -
-
-
- -
-
-
- (Optional)
-
-
-
- -
-
-
- Displayed in the Addon Manager's list of Addons. Should not include the word "FreeCAD", and must be a valid directory name on all support operating systems.
-
-
-
- -
-
-
- (Optional)
-
-
-
- -
-
-
- README URL
-
-
-
- -
-
-
- Explanation of what this Addon provides. Displayed in the Addon Manager. It is not necessary for this to state that this is a FreeCAD Addon.
-
-
- true
-
-
- TIP: Since this is displayed within FreeCAD, in the Addon Manager, it is not necessary to take up space saying things like "This is a FreeCAD Addon..." -- just say what it does.
-
-
-
- -
-
-
- Repository URL
-
-
-
- -
-
-
- (Optional)
-
-
-
- -
-
-
-
-
-
- -
-
-
- -
-
-
- Browse...
-
-
-
-
-
- -
-
-
- Website URL
-
-
-
- -
-
-
- Documentation URL
-
-
-
- -
-
-
- (Optional)
-
-
-
- -
-
-
- Displayed in the Addon Manager's list of Addons. Should not include the word "FreeCAD", and must be a valid directory name on all support operating systems.
-
-
- Addon Name
-
-
-
- -
-
-
- Version
-
-
-
- -
-
-
- (Recommended)
-
-
-
- -
-
-
- Minimum Python
-
-
-
- -
-
-
-
-
-
- (Optional, only 3.x version supported)
-
-
-
- -
-
-
- Detect...
-
-
-
-
-
-
-
-
- -
-
-
- Addon Contents
-
-
-
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- ...
-
-
-
- -
-
-
- ...
-
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Save
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- DeveloperModeDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- DeveloperModeDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_add_content.ui b/src/Mod/AddonManager/developer_mode_add_content.ui
deleted file mode 100644
index 0c6dc65d5a..0000000000
--- a/src/Mod/AddonManager/developer_mode_add_content.ui
+++ /dev/null
@@ -1,419 +0,0 @@
-
-
- addContentDialog
-
-
-
- 0
- 0
- 642
- 593
-
-
-
- Content Item
-
-
- true
-
-
- -
-
-
-
-
-
- Content type:
-
-
-
- -
-
-
-
-
- Macro
-
-
- -
-
- Preference Pack
-
-
- -
-
- Workbench
-
-
-
-
- -
-
-
- If this is the only thing in the Addon, all other metadata can be inherited from the top level, and does not need to be specified here.
-
-
- This is the only item in the Addon
-
-
- true
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
- -
-
-
- 0
-
-
-
-
-
-
-
- Main macro file
-
-
-
- -
-
-
- The file with the macro's metadata in it
-
-
-
- -
-
-
- Browse...
-
-
-
-
-
-
-
- -
-
-
- Preference Pack Name
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- Workbench class name
-
-
-
- -
-
-
- Class that defines "Icon" data member
-
-
-
-
-
-
-
- -
-
-
-
-
-
- Subdirectory
-
-
-
- -
-
-
-
-
-
- Optional, defaults to name of content item
-
-
-
- -
-
-
- Browse...
-
-
-
-
-
- -
-
-
- Icon
-
-
-
- -
-
-
-
-
-
- actualIcon
-
-
-
- -
-
-
- Optional, defaults to inheriting from top-level Addon
-
-
-
- -
-
-
- Browse...
-
-
-
-
-
-
-
- -
-
-
-
-
-
- Tags...
-
-
-
- -
-
-
- Dependencies...
-
-
-
- -
-
-
- FreeCAD Versions...
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 20
- 20
-
-
-
-
- -
-
-
- Other Metadata
-
-
-
-
-
-
- Displayed in the Addon Manager's list of Addons. Should not include the word "FreeCAD".
-
-
-
- -
-
-
- true
-
-
-
- -
-
-
- Version
-
-
-
- -
-
-
- Description
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
-
-
-
- -
-
-
-
-
-
- Semantic (1.2.3-beta) or CalVer (2022.08.30) styles supported
-
-
-
- -
-
-
- Set to today (CalVer style)
-
-
-
-
-
- -
-
-
- Display Name
-
-
-
- -
-
-
- -
-
-
-
- 50
- true
- false
-
-
-
- Any fields left blank are inherited from the top-level Addon metadata, so technically they are all optional. For Addons with multiple content items, each item should provide a unique Display Name and Description.
-
-
- true
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- addContentDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- addContentDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
- addonKindComboBox
- currentIndexChanged(int)
- stackedWidget
- setCurrentIndex(int)
-
-
- 134
- 19
-
-
- 320
- 83
-
-
-
-
- singletonCheckBox
- toggled(bool)
- otherMetadataGroupBox
- setHidden(bool)
-
-
- 394
- 19
-
-
- 320
- 294
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_advanced_freecad_versions.ui b/src/Mod/AddonManager/developer_mode_advanced_freecad_versions.ui
deleted file mode 100644
index adea0c9fcd..0000000000
--- a/src/Mod/AddonManager/developer_mode_advanced_freecad_versions.ui
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
- FreeCADVersionToBranchMapDialog
-
-
-
- 0
- 0
- 400
- 300
-
-
-
- Advanced Version Mapping
-
-
- -
-
-
- Upcoming versions of the FreeCAD Addon Manager will support developers' setting a specific branch or tag for use with a specific version of FreeCAD (e.g. setting a specific tag as the last version of your Addon to support v0.19, etc.)
-
-
- true
-
-
-
- -
-
-
- true
-
-
- QAbstractItemView::SelectRows
-
-
- false
-
-
- true
-
-
-
- FreeCAD Version
-
-
-
-
- Best-available branch, tag, or commit
-
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- ...
-
-
-
- -
-
-
- ...
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- FreeCADVersionToBranchMapDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- FreeCADVersionToBranchMapDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_copyright_info.ui b/src/Mod/AddonManager/developer_mode_copyright_info.ui
deleted file mode 100644
index 6edebdeec3..0000000000
--- a/src/Mod/AddonManager/developer_mode_copyright_info.ui
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
- copyrightInformationDialog
-
-
-
- 0
- 0
- 345
- 93
-
-
-
- Copyright Information
-
-
- -
-
-
-
-
-
- Copyright holder:
-
-
-
- -
-
-
- -
-
-
- Copyright year:
-
-
-
- -
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- copyrightInformationDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- copyrightInformationDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_dependencies.ui b/src/Mod/AddonManager/developer_mode_dependencies.ui
deleted file mode 100644
index 6feb86f535..0000000000
--- a/src/Mod/AddonManager/developer_mode_dependencies.ui
+++ /dev/null
@@ -1,132 +0,0 @@
-
-
- DependencyDialog
-
-
-
- 0
- 0
- 348
- 242
-
-
-
- Dependencies
-
-
- -
-
-
- QAbstractItemView::NoEditTriggers
-
-
- true
-
-
- QAbstractItemView::SingleSelection
-
-
- QAbstractItemView::SelectRows
-
-
- false
-
-
- false
-
-
-
- Dependency type
-
-
-
-
- Name
-
-
-
-
- Optional?
-
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- ...
-
-
-
- -
-
-
- ...
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- DependencyDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- DependencyDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_edit_dependency.ui b/src/Mod/AddonManager/developer_mode_edit_dependency.ui
deleted file mode 100644
index a70cbc0d5b..0000000000
--- a/src/Mod/AddonManager/developer_mode_edit_dependency.ui
+++ /dev/null
@@ -1,123 +0,0 @@
-
-
- EditDependencyDialog
-
-
-
- 0
- 0
- 347
- 236
-
-
-
- Edit Dependency
-
-
- -
-
-
- Dependency Type
-
-
-
- -
-
-
- -
-
-
- Dependency
-
-
-
- -
-
-
-
-
-
- -
-
-
- Package name, if "Other..."
-
-
-
- -
-
-
-
- true
-
-
-
- NOTE: If "Other..." is selected, the package is not in the ALLOWED_PYTHON_PACKAGES.txt file, and will not be automatically installed by the Addon Manager. Submit a PR at <a href="https://github.com/FreeCAD/FreeCAD-addons">https://github.com/FreeCAD/FreeCAD-addons</a> to request addition of a package.
-
-
- true
-
-
- true
-
-
-
-
-
- -
-
-
- If this is an optional dependency, the Addon Manager will offer to install it (when possible), but will not block installation if the user chooses not to, or cannot, install the package.
-
-
- Optional
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- EditDependencyDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- EditDependencyDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_freecad_versions.ui b/src/Mod/AddonManager/developer_mode_freecad_versions.ui
deleted file mode 100644
index 7716c27664..0000000000
--- a/src/Mod/AddonManager/developer_mode_freecad_versions.ui
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
- FreeCADVersionsDialog
-
-
-
- 0
- 0
- 400
- 120
-
-
-
- Supported FreeCAD Versions
-
-
- -
-
-
- Minimum FreeCAD Version Supported
-
-
-
- -
-
-
- Optional
-
-
-
- -
-
-
- Maximum FreeCAD Version Supported
-
-
-
- -
-
-
- Optional
-
-
-
- -
-
-
- Advanced version mapping...
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- FreeCADVersionsDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- FreeCADVersionsDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_license.ui b/src/Mod/AddonManager/developer_mode_license.ui
deleted file mode 100644
index c78774d1ab..0000000000
--- a/src/Mod/AddonManager/developer_mode_license.ui
+++ /dev/null
@@ -1,134 +0,0 @@
-
-
- selectLicenseDialog
-
-
-
- 0
- 0
- 533
- 124
-
-
-
- Select a license
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- 11
-
-
-
- -
-
-
- About...
-
-
-
-
-
- -
-
-
-
-
-
- License name:
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
- Path to license file:
-
-
-
- -
-
-
- (if required by license)
-
-
-
- -
-
-
- Browse...
-
-
-
- -
-
-
- Create...
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- selectLicenseDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- selectLicenseDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_licenses_table.ui b/src/Mod/AddonManager/developer_mode_licenses_table.ui
deleted file mode 100644
index 3863ac5c8a..0000000000
--- a/src/Mod/AddonManager/developer_mode_licenses_table.ui
+++ /dev/null
@@ -1,123 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 400
- 300
-
-
-
- Form
-
-
- -
-
-
-
- 1
- 0
-
-
-
-
- 150
- 0
-
-
-
-
- 0
- 0
-
-
-
- Licenses
-
-
-
-
-
-
- QAbstractItemView::NoEditTriggers
-
-
- QAbstractItemView::SingleSelection
-
-
- QAbstractItemView::SelectRows
-
-
- 2
-
-
- true
-
-
- 60
-
-
- true
-
-
- false
-
-
-
- License
-
-
-
-
- License file
-
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- ...
-
-
- true
-
-
-
- -
-
-
- ...
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_people.ui b/src/Mod/AddonManager/developer_mode_people.ui
deleted file mode 100644
index 1dc0c828fb..0000000000
--- a/src/Mod/AddonManager/developer_mode_people.ui
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
- personDialog
-
-
-
- 0
- 0
- 313
- 118
-
-
-
- Add Person
-
-
- -
-
-
- A maintainer is someone with current commit access on this project. An author is anyone else you'd like to give credit to.
-
-
-
- -
-
-
-
-
-
- Name:
-
-
-
- -
-
-
- -
-
-
- Email:
-
-
-
- -
-
-
- Email is required for maintainers, and optional for authors.
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- personDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- personDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_people_table.ui b/src/Mod/AddonManager/developer_mode_people_table.ui
deleted file mode 100644
index 468ec99416..0000000000
--- a/src/Mod/AddonManager/developer_mode_people_table.ui
+++ /dev/null
@@ -1,116 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 400
- 300
-
-
-
- Form
-
-
- -
-
-
-
- 2
- 0
-
-
-
- People
-
-
-
-
-
-
- QAbstractItemView::NoEditTriggers
-
-
- QAbstractItemView::SingleSelection
-
-
- QAbstractItemView::SelectRows
-
-
- 3
-
-
- true
-
-
- 75
-
-
- true
-
-
- false
-
-
-
- Kind
-
-
-
-
- Name
-
-
-
-
- Email
-
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- ...
-
-
- true
-
-
-
- -
-
-
- ...
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_select_from_list.ui b/src/Mod/AddonManager/developer_mode_select_from_list.ui
deleted file mode 100644
index 371208908c..0000000000
--- a/src/Mod/AddonManager/developer_mode_select_from_list.ui
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
- SelectFromList
-
-
-
- 0
- 0
- 400
- 111
-
-
-
- Dialog
-
-
- -
-
-
- TextLabel
-
-
-
- -
-
-
- -
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- SelectFromList
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- SelectFromList
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/developer_mode_tags.ui b/src/Mod/AddonManager/developer_mode_tags.ui
deleted file mode 100644
index f149afb3dc..0000000000
--- a/src/Mod/AddonManager/developer_mode_tags.ui
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
- Dialog
-
-
-
- 0
- 0
- 400
- 106
-
-
-
- Edit Tags
-
-
- -
-
-
- Comma-separated list of tags describing this item:
-
-
-
- -
-
-
- -
-
-
-
- true
-
-
-
- HINT: Common tags include "Assembly", "FEM", "Mesh", "NURBS", etc.
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- Dialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- Dialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/expanded_view.py b/src/Mod/AddonManager/expanded_view.py
deleted file mode 100644
index 765878bb19..0000000000
--- a/src/Mod/AddonManager/expanded_view.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# -*- coding: utf-8 -*-
-
-################################################################################
-## Form generated from reading UI file 'expanded_view.ui'
-##
-## Created by: Qt User Interface Compiler version 5.15.1
-##
-## WARNING! All changes made in this file will be lost when recompiling UI file!
-################################################################################
-
-from PySide.QtCore import *
-from PySide.QtGui import *
-from PySide.QtWidgets import *
-
-
-class Ui_ExpandedView(object):
- def setupUi(self, ExpandedView):
- if not ExpandedView.objectName():
- ExpandedView.setObjectName("ExpandedView")
- ExpandedView.resize(807, 141)
- sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(ExpandedView.sizePolicy().hasHeightForWidth())
- ExpandedView.setSizePolicy(sizePolicy)
- self.horizontalLayout_2 = QHBoxLayout(ExpandedView)
- self.horizontalLayout_2.setSpacing(2)
- self.horizontalLayout_2.setObjectName("horizontalLayout_2")
- self.horizontalLayout_2.setSizeConstraint(QLayout.SetNoConstraint)
- self.horizontalLayout_2.setContentsMargins(2, 0, 2, 0)
- self.labelIcon = QLabel(ExpandedView)
- self.labelIcon.setObjectName("labelIcon")
- sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
- sizePolicy1.setHorizontalStretch(0)
- sizePolicy1.setVerticalStretch(0)
- sizePolicy1.setHeightForWidth(self.labelIcon.sizePolicy().hasHeightForWidth())
- self.labelIcon.setSizePolicy(sizePolicy1)
- self.labelIcon.setMinimumSize(QSize(48, 48))
- self.labelIcon.setMaximumSize(QSize(48, 48))
- self.labelIcon.setBaseSize(QSize(48, 48))
-
- self.horizontalLayout_2.addWidget(self.labelIcon)
-
- self.horizontalSpacer = QSpacerItem(8, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)
-
- self.horizontalLayout_2.addItem(self.horizontalSpacer)
-
- self.verticalLayout = QVBoxLayout()
- self.verticalLayout.setSpacing(3)
- self.verticalLayout.setObjectName("verticalLayout")
- self.horizontalLayout = QHBoxLayout()
- self.horizontalLayout.setSpacing(10)
- self.horizontalLayout.setObjectName("horizontalLayout")
- self.labelPackageName = QLabel(ExpandedView)
- self.labelPackageName.setObjectName("labelPackageName")
-
- self.horizontalLayout.addWidget(self.labelPackageName)
-
- self.labelVersion = QLabel(ExpandedView)
- self.labelVersion.setObjectName("labelVersion")
- self.labelVersion.setTextFormat(Qt.RichText)
- sizePolicy2 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
- sizePolicy2.setHorizontalStretch(0)
- sizePolicy2.setVerticalStretch(0)
- sizePolicy2.setHeightForWidth(self.labelVersion.sizePolicy().hasHeightForWidth())
- self.labelVersion.setSizePolicy(sizePolicy2)
-
- self.horizontalLayout.addWidget(self.labelVersion)
-
- self.labelTags = QLabel(ExpandedView)
- self.labelTags.setObjectName("labelTags")
-
- 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)
-
- self.verticalLayout.addLayout(self.horizontalLayout)
-
- self.labelDescription = QLabel(ExpandedView)
- self.labelDescription.setObjectName("labelDescription")
- sizePolicy.setHeightForWidth(self.labelDescription.sizePolicy().hasHeightForWidth())
- self.labelDescription.setSizePolicy(sizePolicy)
- self.labelDescription.setTextFormat(Qt.PlainText)
- self.labelDescription.setAlignment(Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop)
- self.labelDescription.setWordWrap(True)
-
- self.verticalLayout.addWidget(self.labelDescription)
-
- self.labelMaintainer = QLabel(ExpandedView)
- self.labelMaintainer.setObjectName("labelMaintainer")
- sizePolicy2.setHeightForWidth(self.labelMaintainer.sizePolicy().hasHeightForWidth())
- self.labelMaintainer.setSizePolicy(sizePolicy2)
- self.labelMaintainer.setAlignment(Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop)
- self.labelMaintainer.setWordWrap(False)
-
- self.verticalLayout.addWidget(self.labelMaintainer)
-
- self.horizontalLayout_2.addLayout(self.verticalLayout)
-
- self.labelStatus = QLabel(ExpandedView)
- self.labelStatus.setObjectName("labelStatus")
- self.labelStatus.setTextFormat(Qt.RichText)
- self.labelStatus.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter)
-
- self.horizontalLayout_2.addWidget(self.labelStatus)
-
- self.retranslateUi(ExpandedView)
-
- QMetaObject.connectSlotsByName(ExpandedView)
-
- # setupUi
-
- def retranslateUi(self, ExpandedView):
- # ExpandedView.setWindowTitle(QCoreApplication.translate("ExpandedView", "Form", None))
- self.labelIcon.setText(QCoreApplication.translate("ExpandedView", "Icon", None))
- self.labelPackageName.setText(
- QCoreApplication.translate("ExpandedView", "Package Name
", None)
- )
- self.labelVersion.setText(QCoreApplication.translate("ExpandedView", "Version", None))
- self.labelTags.setText(QCoreApplication.translate("ExpandedView", "(tags)", None))
- self.labelDescription.setText(
- QCoreApplication.translate("ExpandedView", "Description", None)
- )
- self.labelMaintainer.setText(QCoreApplication.translate("ExpandedView", "Maintainer", None))
- self.labelStatus.setText(
- QCoreApplication.translate("ExpandedView", "Update Available", None)
- )
-
- # retranslateUi
diff --git a/src/Mod/AddonManager/expanded_view.ui b/src/Mod/AddonManager/expanded_view.ui
deleted file mode 100644
index f0034601df..0000000000
--- a/src/Mod/AddonManager/expanded_view.ui
+++ /dev/null
@@ -1,204 +0,0 @@
-
-
- ExpandedView
-
-
-
- 0
- 0
- 807
- 141
-
-
-
-
- 0
- 0
-
-
-
- Form
-
-
-
- 2
-
-
- QLayout::SetNoConstraint
-
-
- 2
-
-
- 0
-
-
- 2
-
-
- 0
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 48
- 48
-
-
-
-
- 48
- 48
-
-
-
-
- 48
- 48
-
-
-
- Icon
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 8
- 20
-
-
-
-
- -
-
-
- 3
-
-
-
-
-
- 10
-
-
-
-
-
- <h1>Package Name</h1>
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Version
-
-
-
- -
-
-
- (tags)
-
-
-
- -
-
-
- labelSort
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Description
-
-
- Qt::PlainText
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
-
-
- true
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Maintainer
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
-
-
- false
-
-
-
-
-
- -
-
-
- UpdateAvailable
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
-
-
-
-
-
diff --git a/src/Mod/AddonManager/first_run.ui b/src/Mod/AddonManager/first_run.ui
deleted file mode 100644
index 31217bddda..0000000000
--- a/src/Mod/AddonManager/first_run.ui
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
- Dialog
-
-
- Qt::ApplicationModal
-
-
-
- 0
- 0
- 520
- 110
-
-
-
- Add-on Manager: Warning!
-
-
- -
-
-
- 30
-
-
- 20
-
-
- 6
-
-
- 20
-
-
- 6
-
-
-
-
-
-
- 100
- 100
-
-
-
-
- 100
- 100
-
-
-
- false
-
-
- :/icons/AddonMgrWithWarning.svg
-
-
-
- -
-
-
- The Add-on Manager provides access to an extensive library of useful third-party FreeCAD extensions. No guarantees can be made regarding their safety or functionality.
-
-
- true
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Expanding
-
-
-
- -
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Expanding
-
-
-
- -
-
-
- Continue
-
-
-
- -
-
-
- Cancel
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Mod/AddonManager/install_to_toolbar.py b/src/Mod/AddonManager/install_to_toolbar.py
deleted file mode 100644
index 9f3ba038b8..0000000000
--- a/src/Mod/AddonManager/install_to_toolbar.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2024 The FreeCAD Project Association AISBL *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# * . *
-# * *
-# ***************************************************************************
-
-"""A collection of functions to handle installing a macro icon to the toolbar."""
-
-import os
-
-import FreeCAD
-import FreeCADGui
-from PySide import QtCore, QtWidgets
-import Addon
-
-translate = FreeCAD.Qt.translate
-
-
-def ask_to_install_toolbar_button(repo: Addon) -> None:
- """Presents a dialog to the user asking if they want to install a toolbar button for
- a particular macro, and walks through that process if they agree to do so."""
- pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
- do_not_show_dialog = pref.GetBool("dontShowAddMacroButtonDialog", False)
- button_exists = check_for_button(repo)
- if not do_not_show_dialog and not button_exists:
- add_toolbar_button_dialog = FreeCADGui.PySideUic.loadUi(
- os.path.join(os.path.dirname(__file__), "add_toolbar_button_dialog.ui")
- )
- add_toolbar_button_dialog.buttonYes.clicked.connect(lambda: install_toolbar_button(repo))
- add_toolbar_button_dialog.buttonNever.clicked.connect(
- lambda: pref.SetBool("dontShowAddMacroButtonDialog", True)
- )
- add_toolbar_button_dialog.exec()
-
-
-def check_for_button(repo: Addon) -> bool:
- """Returns True if a button already exists for this macro, or False if not."""
- command = FreeCADGui.Command.findCustomCommand(repo.macro.filename)
- if not command:
- return False
- custom_toolbars = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar")
- toolbar_groups = custom_toolbars.GetGroups()
- for group in toolbar_groups:
- toolbar = custom_toolbars.GetGroup(group)
- if toolbar.GetString(command, "*") != "*":
- return True
- return False
-
-
-def ask_for_toolbar(repo: Addon, custom_toolbars) -> object:
- """Determine what toolbar to add the icon to. The first time it is called it prompts the
- user to select or create a toolbar. After that, the prompt is optional and can be configured
- via a preference. Returns the pref group for the new toolbar."""
- pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
-
- # In this one spot, default True: if this is the first time we got to
- # this chunk of code, we are always going to ask.
- ask = pref.GetBool("alwaysAskForToolbar", True)
-
- if ask:
- select_toolbar_dialog = FreeCADGui.PySideUic.loadUi(
- os.path.join(os.path.dirname(__file__), "select_toolbar_dialog.ui")
- )
-
- select_toolbar_dialog.comboBox.clear()
-
- for group in custom_toolbars:
- ref = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar/" + group)
- name = ref.GetString("Name", "")
- if name:
- select_toolbar_dialog.comboBox.addItem(name)
- else:
- FreeCAD.Console.PrintWarning(
- f"Custom toolbar {group} does not have a Name element\n"
- )
- new_menubar_option_text = translate("AddonsInstaller", "Create new toolbar")
- select_toolbar_dialog.comboBox.addItem(new_menubar_option_text)
-
- result = select_toolbar_dialog.exec()
- if result == QtWidgets.QDialog.Accepted:
- selection = select_toolbar_dialog.comboBox.currentText()
- if select_toolbar_dialog.checkBox.checkState() == QtCore.Qt.Unchecked:
- pref.SetBool("alwaysAskForToolbar", False)
- else:
- pref.SetBool("alwaysAskForToolbar", True)
- if selection == new_menubar_option_text:
- return create_new_custom_toolbar()
- return get_toolbar_with_name(selection)
- return None
-
- # If none of the above code returned...
- custom_toolbar_name = pref.GetString("CustomToolbarName", "Auto-Created Macro Toolbar")
- toolbar = get_toolbar_with_name(custom_toolbar_name)
- if not toolbar:
- # They told us not to ask, but then the toolbar got deleted... ask anyway!
- ask = pref.RemBool("alwaysAskForToolbar")
- return ask_for_toolbar(repo, custom_toolbars)
- return toolbar
-
-
-def get_toolbar_with_name(name: str) -> object:
- """Try to find a toolbar with a given name. Returns the preference group for the toolbar
- if found, or None if it does not exist."""
- top_group = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar")
- custom_toolbars = top_group.GetGroups()
- for toolbar in custom_toolbars:
- group = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar/" + toolbar)
- group_name = group.GetString("Name", "")
- if group_name == name:
- return group
- return None
-
-
-def create_new_custom_toolbar() -> object:
- """Create a new custom toolbar and returns its preference group."""
-
- # We need two names: the name of the auto-created toolbar, as it will be displayed to the
- # user in various menus, and the underlying name of the toolbar group. Both must be
- # unique.
-
- # First, the displayed name
- custom_toolbar_name = "Auto-Created Macro Toolbar"
- top_group = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar")
- custom_toolbars = top_group.GetGroups()
- name_taken = check_for_toolbar(custom_toolbar_name)
- if name_taken:
- i = 2 # Don't use (1), start at (2)
- while True:
- test_name = custom_toolbar_name + f" ({i})"
- if not check_for_toolbar(test_name):
- custom_toolbar_name = test_name
- i = i + 1
-
- # Second, the toolbar preference group name
- i = 1
- while True:
- new_group_name = "Custom_" + str(i)
- if new_group_name not in custom_toolbars:
- break
- i = i + 1
-
- custom_toolbar = FreeCAD.ParamGet(
- "User parameter:BaseApp/Workbench/Global/Toolbar/" + new_group_name
- )
- custom_toolbar.SetString("Name", custom_toolbar_name)
- custom_toolbar.SetBool("Active", True)
- return custom_toolbar
-
-
-def check_for_toolbar(toolbar_name: str) -> bool:
- """Returns True if the toolbar exists, otherwise False"""
- return get_toolbar_with_name(toolbar_name) is not None
-
-
-def install_toolbar_button(repo: Addon) -> None:
- """If the user has requested a toolbar button be installed, this function is called
- to continue the process and request any additional required information."""
- pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
- custom_toolbar_name = pref.GetString("CustomToolbarName", "Auto-Created Macro Toolbar")
-
- # Default to false here: if the variable hasn't been set, we don't assume
- # that we have to ask, because the simplest is to just create a new toolbar
- # and never ask at all.
- ask = pref.GetBool("alwaysAskForToolbar", False)
-
- # See if there is already a custom toolbar for macros:
- top_group = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar")
- custom_toolbars = top_group.GetGroups()
- if custom_toolbars:
- # If there are already custom toolbars, see if one of them is the one we used last time
- found_toolbar = False
- for toolbar_name in custom_toolbars:
- test_toolbar = FreeCAD.ParamGet(
- "User parameter:BaseApp/Workbench/Global/Toolbar/" + toolbar_name
- )
- name = test_toolbar.GetString("Name", "")
- if name == custom_toolbar_name:
- custom_toolbar = test_toolbar
- found_toolbar = True
- break
- if ask or not found_toolbar:
- # We have to ask the user what to do...
- custom_toolbar = ask_for_toolbar(repo, custom_toolbars)
- if custom_toolbar:
- custom_toolbar_name = custom_toolbar.GetString("Name")
- pref.SetString("CustomToolbarName", custom_toolbar_name)
- else:
- # Create a custom toolbar
- custom_toolbar = FreeCAD.ParamGet(
- "User parameter:BaseApp/Workbench/Global/Toolbar/Custom_1"
- )
- custom_toolbar.SetString("Name", custom_toolbar_name)
- custom_toolbar.SetBool("Active", True)
-
- if custom_toolbar:
- install_macro_to_toolbar(repo, custom_toolbar)
- else:
- FreeCAD.Console.PrintMessage("In the end, no custom toolbar was set, bailing out\n")
-
-
-def find_installed_icon(repo: Addon) -> str:
- """The icon the macro specifies is usually not the actual installed icon, but rather a cached
- copy. This function looks for a file with the same name located in the macro installation
- path."""
- macro_repo_dir = FreeCAD.getUserMacroDir(True)
- if repo.macro.icon:
- basename = os.path.basename(repo.macro.icon)
- # Simple case first: the file is just in the macro directory...
- if os.path.isfile(os.path.join(macro_repo_dir, basename)):
- return os.path.join(macro_repo_dir, basename)
- # More complex: search for it
- for root, dirs, files in os.walk(macro_repo_dir):
- for name in files:
- if name == basename:
- return os.path.join(root, name)
- return ""
- elif repo.macro.xpm:
- return os.path.normpath(os.path.join(macro_repo_dir, repo.macro.name + "_icon.xpm"))
- else:
- return ""
-
-
-def install_macro_to_toolbar(repo: Addon, toolbar: object) -> None:
- """Adds an icon for the given macro to the given toolbar."""
- menuText = repo.display_name
- tooltipText = f"{repo.display_name}"
- if repo.macro.comment:
- tooltipText += f"
{repo.macro.comment}
"
- whatsThisText = repo.macro.comment
- else:
- whatsThisText = translate(
- "AddonsInstaller", "A macro installed with the FreeCAD Addon Manager"
- )
- statustipText = (
- translate("AddonsInstaller", "Run", "Indicates a macro that can be 'run'")
- + " "
- + repo.display_name
- )
- pixmapText = find_installed_icon(repo)
-
- # Add this command to that toolbar
- command_name = FreeCADGui.Command.createCustomCommand(
- repo.macro.filename,
- menuText,
- tooltipText,
- whatsThisText,
- statustipText,
- pixmapText,
- )
- toolbar.SetString(command_name, "FreeCAD")
-
- # Force the toolbars to be recreated
- wb = FreeCADGui.activeWorkbench()
- wb.reloadActive()
-
-
-def remove_custom_toolbar_button(repo: Addon) -> None:
- """If this repo contains a macro, look through the custom commands and
- see if one is set up for this macro. If so, remove it, including any
- toolbar entries."""
-
- command = FreeCADGui.Command.findCustomCommand(repo.macro.filename)
- if not command:
- return
- custom_toolbars = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar")
- toolbar_groups = custom_toolbars.GetGroups()
- for group in toolbar_groups:
- toolbar = custom_toolbars.GetGroup(group)
- if toolbar.GetString(command, "*") != "*":
- toolbar.RemString(command)
-
- FreeCADGui.Command.removeCustomCommand(command)
-
- # Force the toolbars to be recreated
- wb = FreeCADGui.activeWorkbench()
- wb.reloadActive()
diff --git a/src/Mod/AddonManager/loading.html b/src/Mod/AddonManager/loading.html
deleted file mode 100644
index 2664626d5c..0000000000
--- a/src/Mod/AddonManager/loading.html
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
-
-
- Loading...
-
-
- Loading...
-
-
diff --git a/src/Mod/AddonManager/package_details.ui b/src/Mod/AddonManager/package_details.ui
deleted file mode 100644
index 8c86eae0bb..0000000000
--- a/src/Mod/AddonManager/package_details.ui
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
- PackageDetails
-
-
-
- 0
- 0
- 755
- 367
-
-
-
- Form
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
- ...
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Uninstalls a selected macro or workbench
-
-
- Install
-
-
-
- -
-
-
- Uninstall
-
-
-
- -
-
-
- Update
-
-
-
- -
-
-
- Run Macro
-
-
-
- -
-
-
- Change branch
-
-
-
-
-
- -
-
-
- Installation details go here
-
-
- true
-
-
- false
-
-
-
- -
-
-
-
-
-
-
-
diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py
deleted file mode 100644
index e946e14f12..0000000000
--- a/src/Mod/AddonManager/package_list.py
+++ /dev/null
@@ -1,792 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-# ***************************************************************************
-# * *
-# * Copyright (c) 2022-2023 FreeCAD Project Association *
-# * *
-# * This file is part of FreeCAD. *
-# * *
-# * FreeCAD is free software: you can redistribute it and/or modify it *
-# * under the terms of the GNU Lesser General Public License as *
-# * published by the Free Software Foundation, either version 2.1 of the *
-# * License, or (at your option) any later version. *
-# * *
-# * FreeCAD is distributed in the hope that it will be useful, but *
-# * WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
-# * Lesser General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Lesser General Public *
-# * License along with FreeCAD. If not, see *
-# * . *
-# * *
-# ***************************************************************************
-
-"""Defines the PackageList QWidget for displaying a list of Addons."""
-import datetime
-import threading
-
-import FreeCAD
-
-from PySide import QtCore, QtGui, QtWidgets
-
-from Addon import Addon
-
-from compact_view import Ui_CompactView
-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, SortOptions
-from Widgets.addonmanager_widget_view_selector import AddonManagerDisplayStyle
-from Widgets.addonmanager_widget_filter_selector import StatusFilter, Filter
-from Widgets.addonmanager_widget_progress_bar import Progress, WidgetProgressBar
-from addonmanager_licenses import get_license_manager
-
-translate = FreeCAD.Qt.translate
-
-
-# pylint: disable=too-few-public-methods
-
-
-class PackageList(QtWidgets.QWidget):
- """A widget that shows a list of packages and various widgets to control the
- display of the list, including a progress bar that can display and interrupt the load
- process."""
-
- itemSelected = QtCore.Signal(Addon)
- stop_loading = QtCore.Signal()
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self.ui = Ui_PackageList()
- self.ui.setupUi(self)
-
- self.item_filter = PackageListFilter()
- self.ui.listPackages.setModel(self.item_filter)
- self.item_delegate = PackageListItemDelegate(self.ui.listPackages)
- self.ui.listPackages.setItemDelegate(self.item_delegate)
-
- self.ui.listPackages.clicked.connect(self.on_listPackages_clicked)
- 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))
- self.ui.progress_bar.stop_clicked.connect(self.stop_loading)
-
- # Set up the view the same as the last time:
- pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
- package_type = pref.GetInt("PackageTypeSelection", 0)
- status = pref.GetInt("StatusSelection", 0)
- search_string = pref.GetString("SearchString", "")
- self.ui.view_bar.filter_selector.set_contents_filter(package_type)
- 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.setPackageFilter(package_type)
- self.item_filter.setStatusFilter(status)
-
- # Pre-init of other members:
- self.item_model = None
-
- def setModel(self, model):
- """This is a model-view-controller widget: set its model."""
- self.item_model = model
- self.item_filter.setSourceModel(self.item_model)
- 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)
- self.set_view_style(style)
- self.ui.view_bar.view_selector.set_current_view(style)
-
- self.item_filter.setHidePy2(pref.GetBool("HidePy2", False))
- self.item_filter.setHideObsolete(pref.GetBool("HideObsolete", False))
- self.item_filter.setHideNonOSIApproved(pref.GetBool("HideNonOSIApproved", False))
- self.item_filter.setHideNonFSFLibre(pref.GetBool("HideNonFSFFreeLibre", False))
- self.item_filter.setHideNewerFreeCADRequired(
- pref.GetBool("HideNewerFreeCADRequired", False)
- )
- self.item_filter.setHideUnlicensed(pref.GetBool("HideUnlicensed", False))
-
- def select_addon(self, addon_name: str):
- for index, addon in enumerate(self.item_model.repos):
- if addon.name == addon_name:
- row_index = self.item_model.createIndex(index, 0)
- if self.item_filter.filterAcceptsRow(index):
- self.ui.listPackages.setCurrentIndex(row_index)
- else:
- FreeCAD.Console.PrintLog(
- f"Addon {addon_name} is not visible given current "
- "filter: not selecting it."
- )
- return
- FreeCAD.Console.PrintLog(f"Could not find addon '{addon_name}' to select it")
-
- def on_listPackages_clicked(self, index: QtCore.QModelIndex):
- """Determine what addon was selected and emit the itemSelected signal with it as
- an argument."""
- source_selection = self.item_filter.mapToSource(index)
- selected_repo = self.item_model.repos[source_selection.row()]
- self.itemSelected.emit(selected_repo)
-
- def update_status_filter(self, new_filter: Filter) -> None:
- """hide/show rows corresponding to the specified filter"""
-
- self.item_filter.setStatusFilter(new_filter.status_filter)
- self.item_filter.setPackageFilter(new_filter.content_filter)
- pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
- pref.SetInt("StatusSelection", new_filter.status_filter)
- pref.SetInt("PackageTypeSelection", new_filter.content_filter)
- self.item_filter.invalidateFilter()
-
- def set_view_style(self, style: AddonManagerDisplayStyle) -> None:
- """Set the style (compact or expanded) of the list"""
- if self.item_model:
- self.item_model.layoutAboutToBeChanged.emit()
- self.item_delegate.set_view(style)
- if style == AddonManagerDisplayStyle.COMPACT or style == AddonManagerDisplayStyle.COMPOSITE:
- self.ui.listPackages.setSpacing(2)
- self.ui.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerItem)
- self.ui.listPackages.verticalScrollBar().setSingleStep(-1)
- else:
- self.ui.listPackages.setSpacing(5)
- self.ui.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
- self.ui.listPackages.verticalScrollBar().setSingleStep(24)
- if self.item_model:
- self.item_model.layoutChanged.emit()
-
- pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
- pref.SetInt("ViewStyle", style)
-
- def set_loading(self, is_loading: bool) -> None:
- """Set the loading status of this package list: when a package list is loading, it shows
- a progress bar. When it is no longer loading, the bar is hidden and the search bar gets
- the focus."""
- if is_loading:
- self.ui.progress_bar.show()
- else:
- self.ui.progress_bar.hide()
- self.ui.view_bar.search.setFocus()
-
- def update_loading_progress(self, progress: Progress) -> None:
- self.ui.progress_bar.set_progress(progress)
-
-
-class PackageListItemModel(QtCore.QAbstractListModel):
- """The model for use with the PackageList class."""
-
- repos = []
- write_lock = threading.Lock()
-
- DataAccessRole = QtCore.Qt.UserRole
-
- def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
- """The number of rows"""
- if parent.isValid():
- return 0
- return len(self.repos)
-
- def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
- """Only one column, always returns 1."""
- if parent.isValid():
- return 0
- return 1
-
- def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole):
- """Get the data for a given index and role."""
- if not index.isValid():
- return None
- row = index.row()
- if role == QtCore.Qt.ToolTipRole:
- tooltip = ""
- if self.repos[row].repo_type == Addon.Kind.PACKAGE:
- tooltip = translate("AddonsInstaller", "Click for details about package {}").format(
- self.repos[row].display_name
- )
- elif self.repos[row].repo_type == Addon.Kind.WORKBENCH:
- tooltip = translate(
- "AddonsInstaller", "Click for details about workbench {}"
- ).format(self.repos[row].display_name)
- elif self.repos[row].repo_type == Addon.Kind.MACRO:
- tooltip = translate("AddonsInstaller", "Click for details about macro {}").format(
- self.repos[row].display_name
- )
- return tooltip
- 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.Score:
- return self.repos[row].score
-
- def headerData(self, _unused1, _unused2, _role=QtCore.Qt.DisplayRole):
- """No header in this implementation: always returns None."""
- return None
-
- def append_item(self, repo: Addon) -> None:
- """Adds this addon to the end of the model. Thread safe."""
- if repo in self.repos:
- # Cowardly refuse to insert the same repo a second time
- return
- with self.write_lock:
- self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
- self.repos.append(repo)
- self.endInsertRows()
-
- def clear(self) -> None:
- """Clear the model, removing all rows. Thread safe."""
- if self.rowCount() > 0:
- with self.write_lock:
- self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount() - 1)
- self.repos = []
- self.endRemoveRows()
-
- 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):
- if item.name == repo.name:
- with self.write_lock:
- self.repos[index] = repo
- return
-
-
-class CompactView(QtWidgets.QWidget):
- """A single-line view of the package information"""
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self.ui = Ui_CompactView()
- self.ui.setupUi(self)
-
-
-class ExpandedView(QtWidgets.QWidget):
- """A multi-line view of the package information"""
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self.ui = Ui_ExpandedView()
- self.ui.setupUi(self)
-
-
-class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
- """Render the repo data as a formatted region"""
-
- 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
-
- def set_view(self, style: AddonManagerDisplayStyle) -> None:
- """Set the view of to style"""
- 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."""
- self.update_content(index)
- return self.widget.sizeHint()
-
- def update_content(self, index):
- """Creates the display of the content for a given index."""
- repo = index.data(PackageListItemModel.DataAccessRole)
- if self.displayStyle == AddonManagerDisplayStyle.EXPANDED:
- self.widget = self.expanded
- self._setup_expanded_view(repo)
- elif self.displayStyle == AddonManagerDisplayStyle.COMPACT:
- self.widget = self.compact
- self._setup_compact_view(repo)
- elif self.displayStyle == AddonManagerDisplayStyle.COMPOSITE:
- self.widget = self.compact # For now reuse the compact list
- self._setup_composite_view(repo)
- self.widget.adjustSize()
-
- def _setup_expanded_view(self, addon: Addon) -> None:
- self.widget.ui.labelPackageName.setText(f"{addon.display_name}
")
- 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"v{addon.metadata.version}")
- 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"{addon.display_name}")
- 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"v{addon.metadata.version}")
- 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 _setup_composite_view(self, addon: Addon) -> None:
- self.widget.ui.labelPackageName.setText(f"{addon.display_name}")
- 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"v{addon.metadata.version}")
- elif addon.macro:
- self._set_macro_version_label(addon)
- else:
- self.widget.ui.labelVersion.setText("")
- if self.sort_order != SortOptions.Alphabetical:
- self.widget.ui.labelDescription.setText(self._get_sort_label_text(addon))
- else:
- self.widget.ui.labelDescription.setText("")
-
- def _set_package_maintainer_label(self, addon: Addon):
- maintainers = addon.metadata.maintainer
- maintainers_string = ""
- if len(maintainers) == 1:
- maintainers_string = (
- translate("AddonsInstaller", "Maintainer")
- + f": {maintainers[0].name} <{maintainers[0].email}>"
- )
- elif len(maintainers) > 1:
- n = len(maintainers)
- maintainers_string = translate("AddonsInstaller", "Maintainers:", "", n)
- for maintainer in maintainers:
- maintainers_string += f"\n{maintainer.name} <{maintainer.email}>"
- self.widget.ui.labelMaintainer.setText(maintainers_string)
-
- 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 addon.macro.version:
- version_string = addon.macro.version + " "
- if addon.macro.on_wiki:
- version_string += "(wiki)"
- elif addon.macro.on_git:
- version_string += "(git)"
- else:
- version_string += "(unknown source)"
- self.widget.ui.labelVersion.setText("" + version_string + "")
-
- 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.Score:
- return translate("AddonsInstaller", "Score: ") + str(addon.score)
- return ""
-
- def _get_compact_description(self, addon: Addon) -> str:
- description = ""
- if addon.metadata:
- description = addon.metadata.description
- elif addon.macro and addon.macro.comment:
- description = addon.macro.comment
- trimmed_text, _, _ = description.partition(".")
- return trimmed_text.replace("\n", " ")
-
- @staticmethod
- def get_compact_update_string(repo: Addon) -> str:
- """Get a single-line string listing details about the installed version and
- date"""
-
- result = ""
- if repo.status() == Addon.Status.UNCHECKED:
- result = translate("AddonsInstaller", "Installed")
- elif repo.status() == Addon.Status.NO_UPDATE_AVAILABLE:
- result = translate("AddonsInstaller", "Up-to-date")
- elif repo.status() == Addon.Status.UPDATE_AVAILABLE:
- result = translate("AddonsInstaller", "Update available")
- elif repo.status() == Addon.Status.PENDING_RESTART:
- result = translate("AddonsInstaller", "Pending restart")
-
- if repo.is_disabled():
- style = "style='color:" + utils.warning_color_string() + "; font-weight:bold;'"
- result += f" [" + translate("AddonsInstaller", "DISABLED") + "]"
-
- return result
-
- @staticmethod
- def get_expanded_update_string(repo: Addon) -> str:
- """Get a multi-line string listing details about the installed version and
- date"""
-
- result = ""
-
- installed_version_string = ""
- if repo.status() != Addon.Status.NOT_INSTALLED:
- if repo.installed_version or repo.installed_metadata:
- installed_version_string = (
- "
" + translate("AddonsInstaller", "Installed version") + ": "
- )
- if repo.installed_metadata:
- installed_version_string += str(repo.installed_metadata.version)
- elif repo.installed_version:
- installed_version_string += str(repo.installed_version)
- else:
- installed_version_string = "
" + translate("AddonsInstaller", "Unknown version")
-
- installed_date_string = ""
- if repo.updated_timestamp:
- installed_date_string = "
" + translate("AddonsInstaller", "Installed on") + ": "
- installed_date_string += QtCore.QLocale().toString(
- QtCore.QDateTime.fromSecsSinceEpoch(int(round(repo.updated_timestamp, 0))),
- QtCore.QLocale.ShortFormat,
- )
-
- available_version_string = ""
- if repo.metadata:
- available_version_string = (
- "
" + translate("AddonsInstaller", "Available version") + ": "
- )
- available_version_string += str(repo.metadata.version)
-
- if repo.status() == Addon.Status.UNCHECKED:
- result = translate("AddonsInstaller", "Installed")
- result += installed_version_string
- result += installed_date_string
- elif repo.status() == Addon.Status.NO_UPDATE_AVAILABLE:
- result = translate("AddonsInstaller", "Up-to-date")
- result += installed_version_string
- result += installed_date_string
- elif repo.status() == Addon.Status.UPDATE_AVAILABLE:
- result = translate("AddonsInstaller", "Update available")
- result += installed_version_string
- result += installed_date_string
- result += available_version_string
- elif repo.status() == Addon.Status.PENDING_RESTART:
- result = translate("AddonsInstaller", "Pending restart")
-
- if repo.is_disabled():
- style = "style='color:" + utils.warning_color_string() + "; font-weight:bold;'"
- result += (
- f"
[" + translate("AddonsInstaller", "DISABLED") + "]"
- )
-
- return result
-
- def paint(
- self,
- painter: QtGui.QPainter,
- option: QtWidgets.QStyleOptionViewItem,
- _: QtCore.QModelIndex,
- ):
- """Main paint function: renders this widget into a given rectangle,
- successively drawing all of its children."""
- painter.save()
- self.widget.resize(option.rect.size())
- painter.translate(option.rect.topLeft())
- self.widget.render(
- painter, QtCore.QPoint(), QtGui.QRegion(), QtWidgets.QWidget.DrawChildren
- )
- painter.restore()
-
-
-class PackageListFilter(QtCore.QSortFilterProxyModel):
- """Handle filtering the item list on various criteria"""
-
- def __init__(self):
- super().__init__()
- self.package_type = 0 # Default to showing everything
- self.status = 0 # Default to showing any
- self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
- self.hide_obsolete = False
- self.hide_py2 = False
- self.hide_non_OSI_approved = False
- self.hide_non_FSF_libre = False
- self.hide_unlicensed = False
- self.hide_newer_freecad_required = False
-
- def setPackageFilter(
- self, package_type: int
- ) -> 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()
-
- def setStatusFilter(
- self, status: int
- ) -> None: # 0=Any, 1=Installed, 2=Not installed, 3=Update available
- """Sets the status filter to status and refreshes."""
- self.status = status
- self.invalidateFilter()
-
- def setHidePy2(self, hide_py2: bool) -> None:
- """Sets whether to hide Python 2-only Addons"""
- self.hide_py2 = hide_py2
- self.invalidateFilter()
-
- def setHideObsolete(self, hide_obsolete: bool) -> None:
- """Sets whether to hide Addons marked obsolete"""
- self.hide_obsolete = hide_obsolete
- self.invalidateFilter()
-
- def setHideNonOSIApproved(self, hide: bool) -> None:
- """Sets whether to hide Addons with non-OSI-approved licenses"""
- self.hide_non_OSI_approved = hide
- self.invalidateFilter()
-
- def setHideNonFSFLibre(self, hide: bool) -> None:
- """Sets whether to hide Addons with non-FSF-Libre licenses"""
- self.hide_non_FSF_libre = hide
- self.invalidateFilter()
-
- def setHideUnlicensed(self, hide: bool) -> None:
- """Sets whether to hide addons without a specified license"""
- self.hide_unlicensed = hide
- self.invalidateFilter()
-
- def setHideNewerFreeCADRequired(self, hide_nfr: bool) -> None:
- """Sets whether to hide packages that have indicated they need a newer version
- of FreeCAD than the one currently running."""
- 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, 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)"""
-
- index = self.sourceModel().createIndex(row, 0)
- data = self.sourceModel().data(index, PackageListItemModel.DataAccessRole)
- if self.package_type == 1:
- if not data.contains_workbench():
- return False
- elif self.package_type == 2:
- if not data.contains_macro():
- return False
- 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:
- return False
- elif self.status == StatusFilter.NOT_INSTALLED:
- if data.status() != Addon.Status.NOT_INSTALLED:
- return False
- elif self.status == StatusFilter.UPDATE_AVAILABLE:
- if data.status() != Addon.Status.UPDATE_AVAILABLE:
- return False
-
- license_manager = get_license_manager()
- if data.status() == Addon.Status.NOT_INSTALLED:
-
- # If it's not installed, check to see if it's Py2 only
- if self.hide_py2 and data.python2:
- return False
-
- # If it's not installed, check to see if it's marked obsolete
- if self.hide_obsolete and data.obsolete:
- return False
-
- if self.hide_unlicensed:
- if not data.license or data.license in ["UNLICENSED", "UNLICENCED"]:
- FreeCAD.Console.PrintLog(f"Hiding {data.name} because it has no license set\n")
- return False
-
- # If it is not an OSI-approved license, check to see if we are hiding those
- if self.hide_non_OSI_approved or self.hide_non_FSF_libre:
- if not data.license:
- return False
- licenses_to_check = []
- if type(data.license) is str:
- licenses_to_check.append(data.license)
- elif type(data.license) is list:
- for license_id in data.license:
- if type(license_id) is str:
- licenses_to_check.append(license_id)
- else:
- licenses_to_check.append(license_id.name)
- else:
- licenses_to_check.append(data.license.name)
-
- fsf_libre = False
- osi_approved = False
- for license_id in licenses_to_check:
- if not osi_approved and license_manager.is_osi_approved(license_id):
- osi_approved = True
- 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"
- # )
- 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"
- # )
- return False
-
- # If it's not installed, check to see if it's for a newer version of FreeCAD
- if (
- data.status() == Addon.Status.NOT_INSTALLED
- and self.hide_newer_freecad_required
- and data.metadata
- ):
- # Only hide if ALL content items require a newer version, otherwise
- # it's possible that this package actually provides versions of itself
- # for newer and older versions
-
- first_supported_version = get_first_supported_freecad_version(data.metadata)
- if first_supported_version is not None:
- current_fc_version = Version(from_list=FreeCAD.Version())
- if first_supported_version > current_fc_version:
- return False
-
- name = data.display_name
- desc = data.description
- if hasattr(self, "filterRegularExpression"): # Added in Qt 5.12
- re = self.filterRegularExpression()
- if re.isValid():
- re.setPatternOptions(QtCore.QRegularExpression.CaseInsensitiveOption)
- if re.match(name).hasMatch():
- return True
- if re.match(desc).hasMatch():
- return True
- if data.macro and data.macro.comment and re.match(data.macro.comment).hasMatch():
- return True
- for tag in data.tags:
- if re.match(tag).hasMatch():
- return True
- return False
- # Only get here for Qt < 5.12
- re = self.filterRegExp()
- if re.isValid():
- re.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
- if re.indexIn(name) != -1:
- return True
- if re.indexIn(desc) != -1:
- return True
- if data.macro and data.macro.comment and re.indexIn(data.macro.comment) != -1:
- return True
- for tag in data.tags:
- if re.indexIn(tag) != -1:
- return True
- return False
-
-
-# pylint: disable=attribute-defined-outside-init, missing-function-docstring
-
-
-class Ui_PackageList:
- """The contents of the PackageList widget"""
-
- def setupUi(self, form):
- if not form.objectName():
- form.setObjectName("PackageList")
- self.verticalLayout = QtWidgets.QVBoxLayout(form)
- self.verticalLayout.setObjectName("verticalLayout")
- self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
- self.horizontalLayout_6.setObjectName("horizontalLayout_6")
-
- self.view_bar = WidgetViewControlBar(form)
- self.view_bar.setObjectName("ViewControlBar")
- self.horizontalLayout_6.addWidget(self.view_bar)
-
- self.verticalLayout.addLayout(self.horizontalLayout_6)
-
- self.listPackages = QtWidgets.QListView(form)
- self.listPackages.setObjectName("listPackages")
- self.listPackages.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
- self.listPackages.setProperty("showDropIndicator", False)
- self.listPackages.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
- self.listPackages.setResizeMode(QtWidgets.QListView.Adjust)
- self.listPackages.setUniformItemSizes(False)
- self.listPackages.setAlternatingRowColors(True)
- self.listPackages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- self.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
- self.listPackages.verticalScrollBar().setSingleStep(24)
-
- self.verticalLayout.addWidget(self.listPackages)
-
- self.progress_bar = WidgetProgressBar()
- self.verticalLayout.addWidget(self.progress_bar)
-
- QtCore.QMetaObject.connectSlotsByName(form)
diff --git a/src/Mod/AddonManager/proxy_authentication.ui b/src/Mod/AddonManager/proxy_authentication.ui
deleted file mode 100644
index 7c804680b5..0000000000
--- a/src/Mod/AddonManager/proxy_authentication.ui
+++ /dev/null
@@ -1,150 +0,0 @@
-
-
- proxy_authentication
-
-
-
- 0
- 0
- 536
- 276
-
-
-
- Proxy login required
-
-
- -
-
-
- Proxy requires authentication
-
-
-
- -
-
-
-
-
-
- Proxy:
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Placeholder for proxy address
-
-
-
- -
-
-
- Realm:
-
-
-
- -
-
-
- Placeholder for proxy realm
-
-
-
-
-
- -
-
-
-
-
-
- Username
-
-
-
- -
-
-
- -
-
-
- Password
-
-
-
- -
-
-
- QLineEdit::PasswordEchoOnEdit
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- proxy_authentication
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- proxy_authentication
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/select_toolbar_dialog.ui b/src/Mod/AddonManager/select_toolbar_dialog.ui
deleted file mode 100644
index 2dda0cb038..0000000000
--- a/src/Mod/AddonManager/select_toolbar_dialog.ui
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
- select_toolbar_dialog
-
-
-
- 0
- 0
- 308
- 109
-
-
-
- Select Toolbar
-
-
- true
-
-
- true
-
-
- -
-
-
- Select a toolbar to add this macro to:
-
-
-
- -
-
-
- -
-
-
- Ask every time
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- select_toolbar_dialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- select_toolbar_dialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/AddonManager/toolbar_button.ui b/src/Mod/AddonManager/toolbar_button.ui
deleted file mode 100644
index 5765542490..0000000000
--- a/src/Mod/AddonManager/toolbar_button.ui
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
- toolbar_button
-
-
-
- 0
- 0
- 257
- 62
-
-
-
- Add button?
-
-
- -
-
-
- Add a toolbar button for this macro?
-
-
- true
-
-
-
- -
-
-
-
-
-
- Yes
-
-
-
- -
-
-
- No
-
-
-
- -
-
-
- Never
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Mod/AddonManager/update_all.ui b/src/Mod/AddonManager/update_all.ui
deleted file mode 100644
index 27672917d5..0000000000
--- a/src/Mod/AddonManager/update_all.ui
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
- UpdateAllDialog
-
-
-
- 0
- 0
- 400
- 300
-
-
-
- Updating Addons
-
-
- true
-
-
- -
-
-
- Updating out-of-date addons...
-
-
-
- -
-
-
- Qt::ScrollBarAlwaysOff
-
-
- QAbstractScrollArea::AdjustToContents
-
-
- QAbstractItemView::NoEditTriggers
-
-
- false
-
-
- false
-
-
- false
-
-
- QAbstractItemView::NoSelection
-
-
- QAbstractItemView::SelectItems
-
-
- false
-
-
- false
-
-
- true
-
-
- false
-
-
-
- -
-
-
- QDialogButtonBox::Cancel
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- UpdateAllDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- UpdateAllDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/Mod/CMakeLists.txt b/src/Mod/CMakeLists.txt
index 7f0d54c2d4..762264101c 100644
--- a/src/Mod/CMakeLists.txt
+++ b/src/Mod/CMakeLists.txt
@@ -1,4 +1,8 @@
if(BUILD_ADDONMGR)
+ if( NOT EXISTS "${CMAKE_SOURCE_DIR}/src/Mod/AddonManager/CMakeLists.txt" )
+ message(FATAL_ERROR "The Addon Manager has been moved into a git submodule. Please run
+ git submodule update --init" )
+ endif()
add_subdirectory(AddonManager)
endif(BUILD_ADDONMGR)
diff --git a/src/Tools/updatecrowdin.py b/src/Tools/updatecrowdin.py
index 79a4520ad1..d928fcb921 100755
--- a/src/Tools/updatecrowdin.py
+++ b/src/Tools/updatecrowdin.py
@@ -108,11 +108,6 @@ GENERATE_QM = {
# locations list contains Module name, relative path to translation folder and relative path to qrc file
locations = [
- [
- "AddonManager",
- "../Mod/AddonManager/Resources/translations",
- "../Mod/AddonManager/Resources/AddonManager.qrc",
- ],
["App", "../App/Resources/translations", "../App/Resources/App.qrc"],
["Arch", "../Mod/BIM/Resources/translations", "../Mod/BIM/Resources/Arch.qrc"],
[
diff --git a/src/Tools/updatets.py b/src/Tools/updatets.py
index 312d467138..45320f7219 100755
--- a/src/Tools/updatets.py
+++ b/src/Tools/updatets.py
@@ -57,11 +57,6 @@ directories = [
{"tsname": "App", "workingdir": "./src/App", "tsdir": "Resources/translations"},
{"tsname": "Base", "workingdir": "./src/Base", "tsdir": "Resources/translations"},
{"tsname": "FreeCAD", "workingdir": "./src/Gui", "tsdir": "Language"},
- {
- "tsname": "AddonManager",
- "workingdir": "./src/Mod/AddonManager/",
- "tsdir": "Resources/translations",
- },
{
"tsname": "Arch",
"workingdir": "./src/Mod/BIM/",