diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py
index c29a910029..f3f1f61c84 100644
--- a/src/Mod/AddonManager/package_details.py
+++ b/src/Mod/AddonManager/package_details.py
@@ -21,14 +21,16 @@
# * USA *
# * *
# ***************************************************************************
-from posixpath import normpath
+
+""" Provides the PackageDetails widget. """
+
+import os
+from typing import Optional
+
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
-
-import os
-
import FreeCAD
import FreeCADGui
@@ -47,7 +49,6 @@ try:
except ImportError:
pass
-from typing import Optional
translate = FreeCAD.Qt.translate
@@ -57,7 +58,7 @@ try:
from PySide2.QtWebEngineWidgets import *
HAS_QTWEBENGINE = True
-except Exception:
+except ImportError:
FreeCAD.Console.PrintWarning(
translate(
"AddonsInstaller",
@@ -69,6 +70,8 @@ except Exception:
class PackageDetails(QWidget):
+ """ The PackageDetails QWidget shows package README information and provides
+ install, uninstall, and update buttons. """
back = Signal()
install = Signal(Addon)
@@ -104,13 +107,16 @@ class PackageDetails(QWidget):
self.ui.webView.loadFinished.connect(self.load_finished)
loading_html_file = os.path.join(os.path.dirname(__file__), "loading.html")
- with open(loading_html_file, "r", errors="ignore") as f:
+ with open(loading_html_file, "r", errors="ignore", encoding="utf-8") as f:
html = f.read()
self.ui.loadingLabel.setHtml(html)
self.ui.loadingLabel.show()
self.ui.webView.hide()
def show_repo(self, repo: Addon, reload: bool = False) -> None:
+ """ The main entry point for this class, shows the package details and related buttons
+ for the provided repo. If reload is true, then even if this is already the current repo
+ the data is reloaded. """
# If this is the same repo we were already showing, we do not have to do the
# expensive refetch unless reload is true
@@ -162,6 +168,7 @@ class PackageDetails(QWidget):
self.display_repo_status(self.repo.update_status)
def display_repo_status(self, status):
+ """ Updates the contents of the widget to diplay the current install status of the widget. """
repo = self.repo
self.set_change_branch_button_state()
self.set_disable_button_state()
@@ -383,6 +390,10 @@ class PackageDetails(QWidget):
self.ui.labelWarningInfo.hide()
def requires_newer_freecad(self) -> Optional[str]:
+ """ 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.repo.status() == Addon.Status.NOT_INSTALLED and self.repo.metadata:
# Only hide if ALL content items require a newer version, otherwise
@@ -399,7 +410,7 @@ class PackageDetails(QWidget):
if int(required_version[0]) > fc_major:
return first_supported_version
- elif int(required_version[0]) == fc_major and len(required_version) > 1:
+ if int(required_version[0]) == fc_major and len(required_version) > 1:
if int(required_version[1]) > fc_minor:
return first_supported_version
return None
@@ -438,6 +449,7 @@ class PackageDetails(QWidget):
self.ui.buttonChangeBranch.show()
def set_disable_button_state(self):
+ """ Set up the enable/disable button based on the enabled/disabled state of the addon """
self.ui.buttonEnable.hide()
self.ui.buttonDisable.hide()
status = self.repo.status()
@@ -491,6 +503,7 @@ class PackageDetails(QWidget):
self.macro_readme_updated()
def macro_readme_updated(self):
+ """ Update the display of a Macro's README data. """
url = self.repo.macro.wiki
if not url:
url = self.repo.macro.url
@@ -575,13 +588,16 @@ class PackageDetails(QWidget):
self.ui.webView.page().runJavaScript(s)
def load_started(self):
+ """ Called when loading is started: sets up the progress bar """
self.ui.progressBar.show()
self.ui.progressBar.setValue(0)
def load_progress(self, progress: int):
+ """ Called during load to update the progress bar """
self.ui.progressBar.setValue(progress)
def load_finished(self, load_succeeded: bool):
+ """ Once loading is complete, update the dispaly of the progress bar and loading widget. """
self.ui.loadingLabel.hide()
self.ui.slowLoadLabel.hide()
self.ui.webView.show()
@@ -608,12 +624,14 @@ class PackageDetails(QWidget):
self.show_error_for(url)
def long_load_running(self):
+ """ Displays a message about loading taking a long time. """
if hasattr(self.ui, "webView") and self.ui.webView.isHidden():
self.ui.slowLoadLabel.show()
self.ui.loadingLabel.hide()
self.ui.webView.show()
def show_error_for(self, url: QUrl) -> None:
+ """ Displays error information. """
m = translate(
"AddonsInstaller", "Could not load README data from URL {}"
).format(url.toString())
@@ -621,6 +639,7 @@ class PackageDetails(QWidget):
self.ui.webView.setHtml(html)
def change_branch_clicked(self) -> None:
+ """ Loads the branch-switching dialog """
basedir = FreeCAD.getUserAppDataDir()
path_to_repo = os.path.join(basedir, "Mod", self.repo.name)
change_branch_dialog = ChangeBranchDialog(path_to_repo, self)
@@ -628,6 +647,7 @@ class PackageDetails(QWidget):
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.repo.enable()
self.repo.set_status(Addon.Status.PENDING_RESTART)
self.set_disable_button_state()
@@ -644,6 +664,7 @@ class PackageDetails(QWidget):
self.ui.labelWarningInfo.setStyleSheet("color:" + utils.bright_color_string())
def disable_clicked(self) -> None:
+ """ Called by the Disable button, disables this Addon and updates the GUI to reflect that status. """
self.repo.disable()
self.repo.set_status(Addon.Status.PENDING_RESTART)
self.set_disable_button_state()
@@ -662,6 +683,7 @@ class PackageDetails(QWidget):
)
def branch_changed(self, name: str) -> None:
+ """ Displays a dialog confirming the branch changed, and tries to access the metadata file from that branch. """
QMessageBox.information(
self,
translate("AddonsInstaller", "Success"),
@@ -706,6 +728,8 @@ if HAS_QTWEBENGINE:
self.settings().setAttribute(QWebEngineSettings.ErrorPageEnabled, False)
def acceptNavigationRequest(self, url, _type, isMainFrame):
+ """ A callback for navigation requests: this widget will only display navigation requests to the
+ FreeCAD Wiki (for translation purposes) -- anything else will open in a new window. """
if _type == QWebEnginePage.NavigationTypeLinkClicked:
# See if the link is to a FreeCAD Wiki page -- if so, follow it, otherwise ask the OS to open it
@@ -714,12 +738,14 @@ if HAS_QTWEBENGINE:
or url.host() == "wiki.freecadweb.org"
):
return super().acceptNavigationRequest(url, _type, isMainFrame)
- else:
- QDesktopServices.openUrl(url)
- return False
+ QDesktopServices.openUrl(url)
+ return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
- def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID):
+ def javaScriptConsoleMessage(self, level, message, lineNumber, _):
+ """ Handle JavaScript console messages by optionally outputting them to the FreeCAD Console. This
+ must be manually enabled in this Python file by setting the global show_javascript_console_output
+ to true. """
global show_javascript_console_output
if show_javascript_console_output:
tag = translate("AddonsInstaller", "Page JavaScript reported")
@@ -732,6 +758,9 @@ if HAS_QTWEBENGINE:
class Ui_PackageDetails(object):
+ """ The generated UI from the Qt Designer UI file """
+
+
def setupUi(self, PackageDetails):
if not PackageDetails.objectName():
PackageDetails.setObjectName("PackageDetails")
diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py
index 9ffd66ce76..69c644dd8f 100644
--- a/src/Mod/AddonManager/package_list.py
+++ b/src/Mod/AddonManager/package_list.py
@@ -22,15 +22,17 @@
# * *
# ***************************************************************************
+""" Defines the PackageList QWidget for displaying a list of Addons. """
+
+from enum import IntEnum
+import threading
+
import FreeCAD
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
-from enum import IntEnum
-import threading
-
from Addon import Addon
from compact_view import Ui_CompactView
@@ -42,11 +44,13 @@ translate = FreeCAD.Qt.translate
class ListDisplayStyle(IntEnum):
+ """ The display mode of the list """
COMPACT = 0
EXPANDED = 1
class StatusFilter(IntEnum):
+ """ Predefined filers """
ANY = 0
INSTALLED = 1
NOT_INSTALLED = 2
@@ -89,7 +93,11 @@ class PackageList(QWidget):
status = pref.GetInt("StatusSelection", 0)
self.ui.comboStatus.setCurrentIndex(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.sort(0)
@@ -109,6 +117,8 @@ class PackageList(QWidget):
)
def on_listPackages_clicked(self, index: 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)
@@ -166,6 +176,7 @@ class PackageList(QWidget):
self.item_filter.setFilterRegExp(text_filter)
def set_view_style(self, style: ListDisplayStyle) -> None:
+ """ Set the style (compact or expanded) of the list """
self.item_model.layoutAboutToBeChanged.emit()
self.item_delegate.set_view(style)
if style == ListDisplayStyle.COMPACT:
@@ -179,6 +190,7 @@ class PackageList(QWidget):
class PackageListItemModel(QAbstractListModel):
+ """ The model for use with the PackageList class. """
repos = []
write_lock = threading.Lock()
@@ -187,20 +199,20 @@ class PackageListItemModel(QAbstractListModel):
StatusUpdateRole = Qt.UserRole + 1
IconUpdateRole = Qt.UserRole + 2
- def __init__(self, parent=None) -> None:
- super().__init__(parent)
-
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
+ """ The number of rows """
if parent.isValid():
return 0
return len(self.repos)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
+ """ Only one column, always returns 1. """
if parent.isValid():
return 0
return 1
def data(self, index: QModelIndex, role: int = Qt.DisplayRole):
+ """ Get the data for a given index and role. """
if not index.isValid():
return None
row = index.row()
@@ -219,52 +231,53 @@ class PackageListItemModel(QAbstractListModel):
"AddonsInstaller", "Click for details about macro {}"
).format(self.repos[row].display_name)
return tooltip
- elif role == PackageListItemModel.DataAccessRole:
+ if role == PackageListItemModel.DataAccessRole:
return self.repos[row]
- def headerData(self, section, orientation, role=Qt.DisplayRole):
+ def headerData(self, _unused1, _unused2, _role=Qt.DisplayRole):
+ """ No header in this implementation: always returns None. """
return None
def setData(self, index: QModelIndex, value, role=Qt.EditRole) -> None:
"""Set the data for this row. The column of the index is ignored."""
row = index.row()
- self.write_lock.acquire()
- if role == PackageListItemModel.StatusUpdateRole:
- self.repos[row].set_status(value)
- self.dataChanged.emit(
- self.index(row, 2),
- self.index(row, 2),
- [PackageListItemModel.StatusUpdateRole],
- )
- elif role == PackageListItemModel.IconUpdateRole:
- self.repos[row].icon = value
- self.dataChanged.emit(
- self.index(row, 0),
- self.index(row, 0),
- [PackageListItemModel.IconUpdateRole],
- )
- self.write_lock.release()
+ with self.write_lock:
+ if role == PackageListItemModel.StatusUpdateRole:
+ self.repos[row].set_status(value)
+ self.dataChanged.emit(
+ self.index(row, 2),
+ self.index(row, 2),
+ [PackageListItemModel.StatusUpdateRole],
+ )
+ elif role == PackageListItemModel.IconUpdateRole:
+ self.repos[row].icon = value
+ self.dataChanged.emit(
+ self.index(row, 0),
+ self.index(row, 0),
+ [PackageListItemModel.IconUpdateRole],
+ )
def append_item(self, repo: Addon) -> None:
+ """ Adds this addon to the end of the model. Thread safe. """
if repo in self.repos:
# Cowardly refuse to insert the same repo a second time
return
- self.write_lock.acquire()
- self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
- self.repos.append(repo)
- self.endInsertRows()
- self.write_lock.release()
+ with self.write_lock:
+ self.beginInsertRows(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:
- self.write_lock.acquire()
- self.beginRemoveRows(QModelIndex(), 0, self.rowCount() - 1)
- self.repos = []
- self.endRemoveRows()
- self.write_lock.release()
+ with self.write_lock:
+ self.beginRemoveRows(QModelIndex(), 0, self.rowCount() - 1)
+ self.repos = []
+ self.endRemoveRows()
def update_item_status(self, name: str, status: Addon.Status) -> None:
+ """ Set the status of addon with name to status. """
for row, item in enumerate(self.repos):
if item.name == name:
self.setData(
@@ -273,6 +286,7 @@ class PackageListItemModel(QAbstractListModel):
return
def update_item_icon(self, name: str, icon: QIcon) -> None:
+ """ Set the icon for Addon with name to icon """
for row, item in enumerate(self.repos):
if item.name == name:
self.setData(
@@ -281,11 +295,11 @@ class PackageListItemModel(QAbstractListModel):
return
def reload_item(self, repo: Addon) -> None:
+ """ Sets the addon data for the given addon (based on its name) """
for index, item in enumerate(self.repos):
if item.name == repo.name:
- self.write_lock.acquire()
- self.repos[index] = repo
- self.write_lock.release()
+ with self.write_lock:
+ self.repos[index] = repo
return
@@ -322,14 +336,17 @@ class PackageListItemDelegate(QStyledItemDelegate):
self.widget = self.expanded
def set_view(self, style: ListDisplayStyle) -> None:
+ """ Set the view of to style """
if not self.displayStyle == style:
self.displayStyle = style
- def sizeHint(self, option, index):
+ 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 == ListDisplayStyle.EXPANDED:
self.widget = self.expanded
@@ -347,57 +364,9 @@ class PackageListItemDelegate(QStyledItemDelegate):
self.widget.ui.labelDescription.setText(repo.metadata.Description)
self.widget.ui.labelVersion.setText(f"v{repo.metadata.Version}")
if self.displayStyle == ListDisplayStyle.EXPANDED:
- maintainers = repo.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)
- if repo.tags:
- self.widget.ui.labelTags.setText(
- translate("AddonsInstaller", "Tags")
- + ": "
- + ", ".join(repo.tags)
- )
+ self._setup_expanded_package(repo)
elif repo.macro and repo.macro.parsed:
- self.widget.ui.labelDescription.setText(repo.macro.comment)
- version_string = ""
- if repo.macro.version:
- version_string = repo.macro.version + " "
- if repo.macro.on_wiki:
- version_string += "(wiki)"
- elif repo.macro.on_git:
- version_string += "(git)"
- else:
- version_string += "(unknown source)"
- if repo.macro.date:
- version_string = (
- version_string
- + ", "
- + translate("AddonsInstaller", "updated")
- + " "
- + repo.macro.date
- )
- self.widget.ui.labelVersion.setText("" + version_string + "")
- if self.displayStyle == ListDisplayStyle.EXPANDED:
- if repo.macro.author:
- caption = translate("AddonsInstaller", "Author")
- self.widget.ui.labelMaintainer.setText(
- caption + ": " + repo.macro.author
- )
- else:
- self.widget.ui.labelMaintainer.setText("")
+ self._setup_macro(repo)
else:
self.widget.ui.labelDescription.setText("")
self.widget.ui.labelVersion.setText("")
@@ -412,6 +381,62 @@ class PackageListItemDelegate(QStyledItemDelegate):
self.widget.adjustSize()
+ def _setup_expanded_package (self, repo:Addon):
+ """ Set up the display for a package in expanded view """
+ maintainers = repo.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)
+ if repo.tags:
+ self.widget.ui.labelTags.setText(
+ translate("AddonsInstaller", "Tags")
+ + ": "
+ + ", ".join(repo.tags)
+ )
+
+ def _setup_macro(self, repo:Addon):
+ """ Set up the display for a macro """
+ self.widget.ui.labelDescription.setText(repo.macro.comment)
+ version_string = ""
+ if repo.macro.version:
+ version_string = repo.macro.version + " "
+ if repo.macro.on_wiki:
+ version_string += "(wiki)"
+ elif repo.macro.on_git:
+ version_string += "(git)"
+ else:
+ version_string += "(unknown source)"
+ if repo.macro.date:
+ version_string = (
+ version_string
+ + ", "
+ + translate("AddonsInstaller", "updated")
+ + " "
+ + repo.macro.date
+ )
+ self.widget.ui.labelVersion.setText("" + version_string + "")
+ if self.displayStyle == ListDisplayStyle.EXPANDED:
+ if repo.macro.author:
+ caption = translate("AddonsInstaller", "Author")
+ self.widget.ui.labelMaintainer.setText(
+ caption + ": " + repo.macro.author
+ )
+ else:
+ self.widget.ui.labelMaintainer.setText("")
+
def get_compact_update_string(self, repo: Addon) -> str:
"""Get a single-line string listing details about the installed version and date"""
@@ -501,6 +526,8 @@ class PackageListItemDelegate(QStyledItemDelegate):
return result
def paint(self, painter: QPainter, option: QStyleOptionViewItem, _: 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())
@@ -521,36 +548,44 @@ class PackageListFilter(QSortFilterProxyModel):
self.hide_newer_freecad_required = False
def setPackageFilter(
- self, type: int
+ self, package_type: int
) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs
- self.package_type = type
+ """ 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 or not to hide Python 2-only Addons """
self.hide_py2 = hide_py2
self.invalidateFilter()
def setHideObsolete(self, hide_obsolete: bool) -> None:
+ """ Sets whether or not to hide Addons marked obsolete """
self.hide_obsolete = hide_obsolete
self.invalidateFilter()
def setHideNewerFreeCADRequired(self, hide_nfr: bool) -> None:
+ """ Sets whether or not 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, right) -> bool:
+ """ Enable sorting of display name (not case sensitive). """
l = self.sourceModel().data(left, PackageListItemModel.DataAccessRole)
r = self.sourceModel().data(right, PackageListItemModel.DataAccessRole)
return l.display_name.lower() < r.display_name.lower()
def filterAcceptsRow(self, row, parent=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:
@@ -607,7 +642,7 @@ class PackageListFilter(QSortFilterProxyModel):
if int(required_version[0]) > fc_major:
return False
- elif int(required_version[0]) == fc_major and len(required_version) > 1:
+ if int(required_version[0]) == fc_major and len(required_version) > 1:
if int(required_version[1]) > fc_minor:
return False
@@ -630,29 +665,25 @@ class PackageListFilter(QSortFilterProxyModel):
for tag in data.tags:
if re.match(tag).hasMatch():
return True
- return False
- else:
- return False
- else:
- re = self.filterRegExp()
- if re.isValid():
- re.setCaseSensitivity(Qt.CaseInsensitive)
- if re.indexIn(name) != -1:
+ return False
+ # Only get here for Qt < 5.12
+ re = self.filterRegExp()
+ if re.isValid():
+ re.setCaseSensitivity(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
- 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
- else:
- return False
+ return False
class Ui_PackageList(object):