133 lines
6.0 KiB
Python
133 lines
6.0 KiB
Python
# 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 *
|
|
# * <https://www.gnu.org/licenses/>. *
|
|
# * *
|
|
# ***************************************************************************
|
|
import re
|
|
|
|
import FreeCAD
|
|
|
|
# Get whatever version of PySide we can
|
|
try:
|
|
import PySide # Use the FreeCAD wrapper
|
|
except ImportError:
|
|
try:
|
|
import PySide6 # Outside FreeCAD, try Qt6 first
|
|
|
|
PySide = PySide6
|
|
except ImportError:
|
|
import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import)
|
|
|
|
PySide = PySide2
|
|
|
|
from PySide import QtCore, QtGui, QtWidgets
|
|
|
|
from typing import Optional
|
|
|
|
|
|
class WidgetReadmeBrowser(QtWidgets.QTextBrowser):
|
|
"""A QTextBrowser widget that emits signals for each requested image resource, allowing an external controller
|
|
to load and re-deliver those images. Once all resources have been re-delivered, the original data is redisplayed
|
|
with the images in-line. Call setUrl prior to calling setMarkdown or setHtml to ensure URLs are resolved
|
|
correctly."""
|
|
|
|
load_resource = QtCore.Signal(str) # Str is a URL to a resource
|
|
follow_link = QtCore.Signal(str) # Str is a URL to another page
|
|
|
|
def __init__(self, parent: QtWidgets.QWidget = None):
|
|
super().__init__(parent)
|
|
self.image_map = {}
|
|
self.url = ""
|
|
self.stop = False
|
|
self.setOpenExternalLinks(True)
|
|
|
|
def setUrl(self, url: str):
|
|
"""Set the base URL of the page. Used to resolve relative URLs in the page source."""
|
|
self.url = url
|
|
|
|
def setMarkdown(self, md: str):
|
|
"""Provides an optional fallback to the markdown library for older versions of Qt (prior to 5.15) that did not
|
|
have native markdown support. Lacking that, plaintext is displayed."""
|
|
geometry = self.geometry()
|
|
if hasattr(super(), "setMarkdown"):
|
|
|
|
super().setMarkdown(self._clean_markdown(md))
|
|
else:
|
|
try:
|
|
import markdown
|
|
|
|
html = markdown.markdown(md)
|
|
self.setHtml(html)
|
|
except ImportError:
|
|
self.setText(md)
|
|
FreeCAD.Console.Warning(
|
|
"Qt < 5.15 and no `import markdown` -- falling back to plain text display\n"
|
|
)
|
|
self.setGeometry(geometry)
|
|
|
|
def _clean_markdown(self, md: str):
|
|
# Remove some HTML tags ( for now just img and br, which are the most common offenders that break rendering )
|
|
br_re = re.compile(r"<br\s*/?>")
|
|
img_re = re.compile(r"<img\s.*?src=(?:'|\")([^'\">]+)(?:'|\").*?\/?>")
|
|
|
|
cleaned = br_re.sub(r"\n", md)
|
|
cleaned = img_re.sub(r"[html tag removed]", cleaned)
|
|
|
|
return cleaned
|
|
|
|
def set_resource(self, resource_url: str, image: Optional[QtGui.QImage]):
|
|
"""Once a resource has been fetched (or the fetch has failed), this method should be used to inform the widget
|
|
that the resource has been loaded. Note that the incoming image is scaled to 97% of the widget width if it is
|
|
larger than that."""
|
|
self.image_map[resource_url] = self._ensure_appropriate_width(image)
|
|
|
|
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 and not self.stop:
|
|
full_url = self._create_full_url(name.toString())
|
|
if full_url not in self.image_map:
|
|
self.load_resource.emit(full_url)
|
|
self.image_map[full_url] = None
|
|
return self.image_map[full_url]
|
|
elif resource_type == QtGui.QTextDocument.MarkdownResource:
|
|
self.follow_link.emit(name.toString())
|
|
return self.toMarkdown()
|
|
elif resource_type == QtGui.QTextDocument.HtmlResource:
|
|
self.follow_link.emit(name.toString())
|
|
return self.toHtml()
|
|
return super().loadResource(resource_type, name)
|
|
|
|
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)
|
|
|
|
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
|