diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index dc9e527c49..bedb988620 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -28,6 +28,7 @@ SET(AddonManager_SRCS
addonmanager_macro_parser.py
addonmanager_metadata.py
addonmanager_pyside_interface.py
+ addonmanager_readme_viewer.py
addonmanager_update_all_gui.py
addonmanager_uninstaller.py
addonmanager_uninstaller_gui.py
diff --git a/src/Mod/AddonManager/addonmanager_readme_viewer.py b/src/Mod/AddonManager/addonmanager_readme_viewer.py
new file mode 100644
index 0000000000..5e8ec50464
--- /dev/null
+++ b/src/Mod/AddonManager/addonmanager_readme_viewer.py
@@ -0,0 +1,119 @@
+# 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 Addon
+from PySide import QtCore, QtGui, QtWidgets
+
+import addonmanager_freecad_interface as fci
+import addonmanager_utilities as utils
+import NetworkManager
+
+translate = fci.translate
+
+
+class ReadmeViewer(QtWidgets.QTextBrowser):
+
+ """A QTextBrowser widget that, when given an Addon, downloads the README data as appropriate
+ and renders it with whatever technology is available (usually Qt's Markdown renderer for
+ workbenches and its HTML renderer for Macros)."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ NetworkManager.InitializeNetworkManager()
+ NetworkManager.AM_NETWORK_MANAGER.completed.connect(self._download_completed)
+ self.request_index = 0
+ self.url = ""
+ self.repo: Addon.Addon = None
+ self.setOpenExternalLinks(True)
+ self.setOpenLinks(True)
+ self.image_map = {}
+
+ def set_addon(self, repo: Addon):
+ """Set which Addon's information is displayed"""
+
+ self.setPlainText(translate("AddonsInstaller", "Loading README data..."))
+ self.repo = repo
+ if self.repo.repo_type == Addon.Addon.Kind.MACRO:
+ self.url = self.repo.macro.wiki
+ if not self.url:
+ self.url = self.repo.macro.url
+ else:
+ self.url = utils.get_readme_url(repo)
+
+ self.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.request_index:
+ if code == 200: # HTTP success
+ self._process_package_download(data.data().decode("utf-8"))
+ else:
+ self.setPlainText(
+ translate(
+ "AddonsInstaller",
+ "Failed to download data from {} -- received response code {}.",
+ ).format(self.url, code)
+ )
+
+ def _process_package_download(self, data: str):
+ if self.repo.repo_type == Addon.Addon.Kind.MACRO:
+ self.setHtml(data)
+ else:
+ if hasattr(self, "setMarkdown"):
+ self.setMarkdown(data)
+ else:
+ self.setPlainText(data)
+
+ def loadResource(self, resource_type: int, name: QtCore.QUrl) -> object:
+ """Callback for resource loading. Called automatically by underlying Qt
+ code when external resources are needed for rendering. In particular,
+ here it is used to download and cache (in RAM) the images needed for the
+ README and Wiki pages."""
+ if resource_type == QtGui.QTextDocument.ImageResource:
+ full_url = self._create_full_url(name.toString())
+ if full_url not in self.image_map:
+ self.image_map[full_url] = None
+ fci.Console.PrintMessage(f"Downloading image from {full_url}...\n")
+ data = NetworkManager.AM_NETWORK_MANAGER.blocking_get(full_url)
+ if data and data.data():
+ image = QtGui.QImage.fromData(data.data())
+ if image:
+ self.image_map[full_url] = self._ensure_appropriate_width(image)
+ return self.image_map[full_url]
+ return super().loadResource(resource_type, name)
+
+ 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 _ensure_appropriate_width(self, image: QtGui.QImage) -> QtGui.QImage:
+ ninety_seven_percent = self.width() * 0.97
+ if image.width() < ninety_seven_percent:
+ return image
+ return image.scaledToWidth(ninety_seven_percent)
diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py
index 36952b7bd3..93dfb8bee9 100644
--- a/src/Mod/AddonManager/package_details.py
+++ b/src/Mod/AddonManager/package_details.py
@@ -33,6 +33,7 @@ import addonmanager_freecad_interface as fci
import addonmanager_utilities as utils
from addonmanager_metadata import Version, UrlType, get_first_supported_freecad_version
from addonmanager_workers_startup import GetMacroDetailsWorker, CheckSingleUpdateWorker
+from addonmanager_readme_viewer import ReadmeViewer
from Addon import Addon
from change_branch import ChangeBranchDialog
@@ -50,20 +51,6 @@ translate = fci.translate
show_javascript_console_output = False
-try:
- from PySide import QtWebEngineWidgets
-
- HAS_QTWEBENGINE = True
-except ImportError:
- fci.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "Addon Manager Warning: Could not import QtWebEngineWidgets -- README data will display as text-only",
- )
- + "\n"
- )
- HAS_QTWEBENGINE = False
-
class PackageDetails(QtWidgets.QWidget):
"""The PackageDetails QWidget shows package README information and provides
@@ -95,17 +82,6 @@ class PackageDetails(QtWidgets.QWidget):
self.ui.buttonChangeBranch.clicked.connect(self.change_branch_clicked)
self.ui.buttonEnable.clicked.connect(self.enable_clicked)
self.ui.buttonDisable.clicked.connect(self.disable_clicked)
- if HAS_QTWEBENGINE:
- self.ui.webView.loadStarted.connect(self.load_started)
- self.ui.webView.loadProgress.connect(self.load_progress)
- 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", 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
@@ -117,16 +93,6 @@ class PackageDetails(QtWidgets.QWidget):
if self.repo != repo or reload:
self.repo = repo
- if HAS_QTWEBENGINE:
- self.ui.loadingLabel.show()
- self.ui.slowLoadLabel.hide()
- self.ui.webView.setHtml("
Loading...")
- self.ui.webView.hide()
- self.ui.progressBar.show()
- self.timeout = QtCore.QTimer.singleShot(6000, self.long_load_running) # Six seconds
- else:
- self.ui.missingWebViewLabel.setStyleSheet("color:" + utils.warning_color_string())
-
if self.worker is not None:
if not self.worker.isFinished():
self.worker.requestInterruption()
@@ -420,33 +386,13 @@ class PackageDetails(QtWidgets.QWidget):
def show_workbench(self, repo: Addon) -> None:
"""loads information of a given workbench"""
- url = utils.get_readme_html_url(repo)
- if HAS_QTWEBENGINE:
- self.ui.webView.load(QtCore.QUrl(url))
- self.ui.urlBar.setText(url)
- else:
- readme_data = utils.blocking_get(url)
- text = readme_data.decode("utf8")
- self.ui.textBrowserReadMe.setHtml(text)
+
+ self.ui.textBrowserReadMe.set_addon(repo)
def show_package(self, repo: Addon) -> None:
"""Show the details for a package (a repo with a package.xml metadata file)"""
- readme_url = None
- if repo.metadata:
- for url in repo.metadata.url:
- if url.type == UrlType.readme:
- readme_url = url.location
- break
- if not readme_url:
- readme_url = utils.get_readme_html_url(repo)
- if HAS_QTWEBENGINE:
- self.ui.webView.load(QtCore.QUrl(readme_url))
- self.ui.urlBar.setText(readme_url)
- else:
- readme_data = utils.blocking_get(readme_url)
- text = readme_data.decode("utf8")
- self.ui.textBrowserReadMe.setHtml(text)
+ self.ui.textBrowserReadMe.set_addon(repo)
def show_macro(self, repo: Addon) -> None:
"""loads information of a given macro"""
@@ -461,136 +407,8 @@ class PackageDetails(QtWidgets.QWidget):
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
- if HAS_QTWEBENGINE:
- if url:
- self.ui.webView.load(QtCore.QUrl(url))
- self.ui.urlBar.setText(url)
- else:
- self.ui.urlBar.setText(
- "("
- + translate("AddonsInstaller", "No URL or wiki page provided by this macro")
- + ")"
- )
- else:
- if url:
- readme_data = utils.blocking_get(url)
- text = readme_data.decode("utf8")
- self.ui.textBrowserReadMe.setHtml(text)
- else:
- self.ui.textBrowserReadMe.setHtml(
- "("
- + translate("AddonsInstaller", "No URL or wiki page provided by this macro")
- + ")"
- )
-
- def run_javascript(self):
- """Modify the page for a README to optimize for viewing in a smaller window"""
-
- s = """
-( function() {
- const url = new URL (window.location);
- const body = document.getElementsByTagName("body")[0];
- if (url.hostname === "github.com") {
- const articles = document.getElementsByTagName("article");
- if (articles.length > 0) {
- const article = articles[0];
- body.appendChild (article);
- body.style.padding = "1em";
- let sibling = article.previousSibling;
- while (sibling) {
- sibling.remove();
- sibling = article.previousSibling;
- }
- }
- } else if (url.hostname === "gitlab.com" ||
- url.hostname === "framagit.org" ||
- url.hostname === "salsa.debian.org") {
- // These all use the GitLab page display...
- const articles = document.getElementsByTagName("article");
- if (articles.length > 0) {
- const article = articles[0];
- body.appendChild (article);
- body.style.padding = "1em";
- let sibling = article.previousSibling;
- while (sibling) {
- sibling.remove();
- sibling = article.previousSibling;
- }
- }
- } else if (url.hostname === "wiki.freecad.org" ||
- url.hostname === "wiki.freecad.org") {
- const first_heading = document.getElementById('firstHeading');
- const body_content = document.getElementById('bodyContent');
- const new_node = document.createElement("div");
- new_node.appendChild(first_heading);
- new_node.appendChild(body_content);
- body.appendChild(new_node);
- let sibling = new_node.previousSibling;
- while (sibling) {
- sibling.remove();
- sibling = new_node.previousSibling;
- }
- }
-}
-) ()
-"""
- 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 display of the progress bar and loading widget."""
- self.ui.loadingLabel.hide()
- self.ui.slowLoadLabel.hide()
- self.ui.webView.show()
- self.ui.progressBar.hide()
- url = self.ui.webView.url()
- if (
- hasattr(self, "timeout")
- and hasattr(self.timeout, "isActive")
- and self.timeout.isActive()
- ):
- self.timeout.stop()
- if load_succeeded:
- # It says it succeeded, but it might have only succeeded in loading a
- # "Page not found" page!
- title = self.ui.webView.title()
- path_components = url.path().split("/")
- expected_content = path_components[-1]
- if url.host() == "github.com" and expected_content not in title:
- self.show_error_for(url)
- elif title == "":
- self.show_error_for(url)
- else:
- self.run_javascript()
- else:
- 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: QtCore.QUrl) -> None:
- """Displays error information."""
- m = translate("AddonsInstaller", "Could not load README data from URL {}").format(
- url.toString()
- )
- html = f"{m}
"
- self.ui.webView.setHtml(html)
+ self.ui.textBrowserReadMe.set_addon(self.repo)
def change_branch_clicked(self) -> None:
"""Loads the branch-switching dialog"""
@@ -671,57 +489,6 @@ class PackageDetails(QtWidgets.QWidget):
self.update_status.emit(self.repo)
-if HAS_QTWEBENGINE:
-
- class RestrictedWebPage(QtWebEngineWidgets.QWebEnginePage):
- """A class that follows links to FreeCAD wiki pages, but opens all other
- clicked links in the system web browser"""
-
- def __init__(self, parent):
- super().__init__(parent)
- self.settings().setAttribute(
- QtWebEngineWidgets.QWebEngineSettings.ErrorPageEnabled, False
- )
- self.stored_url = None
-
- def acceptNavigationRequest(self, requested_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 == QtWebEngineWidgets.QWebEnginePage.NavigationTypeLinkClicked:
- # See if the link is to a FreeCAD Wiki page -- if so, follow it,
- # otherwise ask the OS to open it
- if (
- requested_url.host() == "wiki.freecad.org"
- or requested_url.host() == "wiki.freecad.org"
- ):
- return super().acceptNavigationRequest(requested_url, _type, isMainFrame)
- QtGui.QDesktopServices.openUrl(requested_url)
- self.stored_url = self.url()
- QtCore.QTimer.singleShot(0, self._reload_stored_url)
- return False
- return super().acceptNavigationRequest(requested_url, _type, isMainFrame)
-
- 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")
- if level == QtWebEngineWidgets.QWebEnginePage.InfoMessageLevel:
- fci.Console.PrintMessage(f"{tag} {lineNumber}: {message}\n")
- elif level == QtWebEngineWidgets.QWebEnginePage.WarningMessageLevel:
- fci.Console.PrintWarning(f"{tag} {lineNumber}: {message}\n")
- elif level == QtWebEngineWidgets.QWebEnginePage.ErrorMessageLevel:
- fci.Console.PrintError(f"{tag} {lineNumber}: {message}\n")
-
- def _reload_stored_url(self):
- if self.stored_url:
- self.load(self.stored_url)
-
-
class Ui_PackageDetails(object):
"""The generated UI from the Qt Designer UI file"""
@@ -810,48 +577,10 @@ class Ui_PackageDetails(object):
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
- if HAS_QTWEBENGINE:
- self.webView = QtWebEngineWidgets.QWebEngineView(PackageDetails)
- self.webView.setObjectName("webView")
- self.webView.setSizePolicy(sizePolicy1)
- self.webView.setPage(RestrictedWebPage(PackageDetails))
+ self.textBrowserReadMe = ReadmeViewer(PackageDetails)
+ self.textBrowserReadMe.setObjectName("textBrowserReadMe")
- self.verticalLayout_2.addWidget(self.webView)
-
- self.loadingLabel = QtWebEngineWidgets.QWebEngineView(PackageDetails)
- self.loadingLabel.setObjectName("loadingLabel")
- self.loadingLabel.setSizePolicy(sizePolicy1)
-
- self.verticalLayout_2.addWidget(self.loadingLabel)
-
- self.slowLoadLabel = QtWidgets.QLabel(PackageDetails)
- self.slowLoadLabel.setObjectName("slowLoadLabel")
-
- self.verticalLayout_2.addWidget(self.slowLoadLabel)
-
- self.progressBar = QtWidgets.QProgressBar(PackageDetails)
- self.progressBar.setObjectName("progressBar")
- self.progressBar.setTextVisible(False)
-
- self.verticalLayout_2.addWidget(self.progressBar)
-
- self.urlBar = QtWidgets.QLineEdit(PackageDetails)
- self.urlBar.setObjectName("urlBar")
- self.urlBar.setReadOnly(True)
-
- self.verticalLayout_2.addWidget(self.urlBar)
- else:
- self.missingWebViewLabel = QtWidgets.QLabel(PackageDetails)
- self.missingWebViewLabel.setObjectName("missingWebViewLabel")
- self.missingWebViewLabel.setWordWrap(True)
- self.verticalLayout_2.addWidget(self.missingWebViewLabel)
-
- self.textBrowserReadMe = QtWidgets.QTextBrowser(PackageDetails)
- self.textBrowserReadMe.setObjectName("textBrowserReadMe")
- self.textBrowserReadMe.setOpenExternalLinks(True)
- self.textBrowserReadMe.setOpenLinks(True)
-
- self.verticalLayout_2.addWidget(self.textBrowserReadMe)
+ self.verticalLayout_2.addWidget(self.textBrowserReadMe)
self.retranslateUi(PackageDetails)
@@ -888,22 +617,5 @@ class Ui_PackageDetails(object):
self.buttonBack.setToolTip(
QtCore.QCoreApplication.translate("AddonsInstaller", "Return to package list", None)
)
- if not HAS_QTWEBENGINE:
- self.missingWebViewLabel.setText(
- ""
- + QtCore.QCoreApplication.translate(
- "AddonsInstaller",
- "QtWebEngine Python bindings not installed -- using fallback README display.",
- None,
- )
- + "
"
- )
- else:
- self.slowLoadLabel.setText(
- QtCore.QCoreApplication.translate(
- "AddonsInstaller",
- "The page is taking a long time to load... showing the data we have so far...",
- )
- )
# retranslateUi