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):