Addon Manager: Switch to QWebEngineView for README

Rather than manually parsing the output from the repo's host, the Addon Manager
now uses an embedded QWebEngineView to display the README data. This makes the
display more repo-location agnostic (rather than trying to parse the specific
code from GitHub, Gitlab, etc.). Special handling of known hosts is still
provided to improve the display of the page, but it is not required. Clicking a
link on the page still loads in a new system browser window, with the exception
of links to the FreeCAD wiki, which are loaded in the same browser. This is
expected to be used primarily to access traslated pages for Macros, so no
advanced web-browsing features are displayed (e.g. back buttons, history
access, etc.).
This commit is contained in:
Chris Hennes
2022-01-28 21:46:36 -06:00
parent efdaed9397
commit 994071c43b
8 changed files with 533 additions and 587 deletions

View File

@@ -87,7 +87,6 @@ class CommandAddonManager:
"update_metadata_cache_worker",
"load_macro_metadata_worker",
"update_all_worker",
"update_check_single_worker",
"dependency_installation_worker",
]
@@ -370,9 +369,6 @@ class CommandAddonManager:
# connect slots
self.dialog.rejected.connect(self.reject)
self.dialog.buttonUpdateAll.clicked.connect(self.update_all)
self.dialog.buttonCheckForUpdates.clicked.connect(
self.manually_check_for_updates
)
self.dialog.buttonClose.clicked.connect(self.dialog.reject)
self.dialog.buttonUpdateCache.clicked.connect(self.on_buttonUpdateCache_clicked)
self.dialog.buttonPauseUpdate.clicked.connect(self.stop_update)
@@ -384,7 +380,6 @@ class CommandAddonManager:
self.packageDetails.update.connect(self.update)
self.packageDetails.back.connect(self.on_buttonBack_clicked)
self.packageDetails.update_status.connect(self.status_updated)
self.packageDetails.check_for_update.connect(self.check_for_update)
# center the dialog over the FreeCAD window
mw = FreeCADGui.getMainWindow()
@@ -759,7 +754,7 @@ class CommandAddonManager:
def status_updated(self, repo: AddonManagerRepo) -> None:
self.item_model.reload_item(repo)
if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
if repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
self.packages_with_updates.append(repo)
self.enable_updates(len(self.packages_with_updates))
@@ -881,7 +876,7 @@ class CommandAddonManager:
missing_external_addons = []
for dep in deps.required_external_addons:
if dep.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
if dep.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
missing_external_addons.append(dep)
# Now check the loaded addons to see if we are missing an internal workbench:
@@ -1181,62 +1176,16 @@ class CommandAddonManager:
def update(self, repo: AddonManagerRepo) -> None:
self.install(repo)
def check_for_update(self, repo: AddonManagerRepo) -> None:
"""Check a single repo for available updates asynchronously"""
if (
hasattr(self, "update_check_single_worker")
and self.update_check_single_worker
):
if self.update_check_single_worker.isRunning():
self.update_check_single_worker.blockSignals(True)
self.update_check_single_worker.requestInterrupt()
self.update_check_single_worker.wait()
self.update_check_single_worker = CheckSingleWorker(repo.name)
self.update_check_single_worker.updateAvailable.connect(
lambda update_available: self.mark_repo_update_available(
repo, update_available
)
)
self.update_check_single_worker.start()
def mark_repo_update_available(
self, repo: AddonManagerRepo, available: bool
) -> None:
if available:
repo.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE
repo.set_status(AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE)
else:
repo.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
repo.set_status(AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE)
self.item_model.reload_item(repo)
self.packageDetails.show_repo(repo)
def manually_check_for_updates(self) -> None:
if hasattr(self, "check_worker"):
thread = self.check_worker
if thread:
if not thread.isFinished():
self.do_next_startup_phase()
return
self.dialog.buttonCheckForUpdates.setText(
translate("AddonsInstaller", "Checking for updates...")
)
self.dialog.buttonCheckForUpdates.setEnabled(False)
self.show_progress_widgets()
self.current_progress_region = 1
self.number_of_progress_regions = 1
self.check_worker = CheckWorkbenchesForUpdatesWorker(self.item_model.repos)
self.check_worker.finished.connect(self.manual_update_check_complete)
self.check_worker.progress_made.connect(self.update_progress_bar)
self.check_worker.update_status.connect(self.status_updated)
self.check_worker.start()
def manual_update_check_complete(self) -> None:
self.dialog.buttonUpdateAll.show()
self.dialog.buttonCheckForUpdates.hide()
self.enable_updates(len(self.packages_with_updates))
self.hide_progress_widgets()
def update_all(self) -> None:
"""Asynchronously apply all available updates: individual failures are noted, but do not stop other updates"""
@@ -1325,11 +1274,9 @@ class CommandAddonManager:
for installed_repo in self.subupdates_succeeded:
if not installed_repo.repo_type == AddonManagerRepo.RepoType.MACRO:
self.restart_required = True
installed_repo.update_status = (
AddonManagerRepo.UpdateStatus.PENDING_RESTART
)
installed_repo.set_status(AddonManagerRepo.UpdateStatus.PENDING_RESTART)
else:
installed_repo.update_status = (
installed_repo.set_status(
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
)
self.item_model.reload_item(installed_repo)
@@ -1404,10 +1351,10 @@ class CommandAddonManager:
QtWidgets.QMessageBox.Close,
)
if repo.repo_type != AddonManagerRepo.RepoType.MACRO:
repo.update_status = AddonManagerRepo.UpdateStatus.PENDING_RESTART
repo.set_status(AddonManagerRepo.UpdateStatus.PENDING_RESTART)
self.restart_required = True
else:
repo.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
repo.set_status(AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE)
self.item_model.reload_item(repo)
self.packageDetails.show_repo(repo)
@@ -1504,13 +1451,19 @@ class CommandAddonManager:
)
# Second, run the Addon's "uninstall.py" script, if it exists
uninstall_script = os.path.join(clonedir,"uninstall.py")
uninstall_script = os.path.join(clonedir, "uninstall.py")
if os.path.exists(uninstall_script):
try:
with open(uninstall_script, 'r') as f:
with open(uninstall_script, "r") as f:
exec(f.read())
except Exception:
FreeCAD.Console.PrintError(translate("AddonsInstaller","Execution of Addon's uninstall.py script failed. Proceeding with uninstall...") + "\n")
FreeCAD.Console.PrintError(
translate(
"AddonsInstaller",
"Execution of Addon's uninstall.py script failed. Proceeding with uninstall...",
)
+ "\n"
)
if os.path.exists(clonedir):
shutil.rmtree(clonedir, onerror=self.remove_readonly)

View File

@@ -23,9 +23,12 @@
import FreeCAD
import os
from typing import Dict, Set, List
from urllib.parse import urlparse
from typing import Dict, Set
from threading import Lock
from addonmanager_macro import Macro
import addonmanager_utilities as utils
translate = FreeCAD.Qt.translate
@@ -54,6 +57,7 @@ class AddonManagerRepo:
NO_UPDATE_AVAILABLE = 2
UPDATE_AVAILABLE = 3
PENDING_RESTART = 4
CANNOT_CHECK = 5 # If we don't have git, etc.
def __lt__(self, other):
if self.__class__ is other.__class__:
@@ -71,6 +75,8 @@ class AddonManagerRepo:
return "Update available"
elif self.value == 4:
return "Restart required"
elif self.value == 5:
return "Can't check"
class Dependencies:
def __init__(self):
@@ -90,16 +96,32 @@ class AddonManagerRepo:
self.display_name = self.name
self.url = url.strip()
self.branch = branch.strip()
self.update_status = status
self.python2 = False
self.obsolete = False
self.rejected = False
self.repo_type = AddonManagerRepo.RepoType.WORKBENCH
self.description = None
self.tags = set() # Just a cache, loaded from Metadata
# To prevent multiple threads from running git actions on this repo at the same time
self.git_lock = Lock()
# To prevent multiple threads from accessing the status at the same time
self.status_lock = Lock()
self.set_status(status)
from addonmanager_utilities import construct_git_url
if "github" in self.url or "gitlab" in self.url or "salsa" in self.url:
# The url should never end in ".git", so strip it if it's there
parsed_url = urlparse(self.url)
if parsed_url.path.endswith(".git"):
self.url = parsed_url.scheme + parsed_url.path[:-4]
if parsed_url.query:
self.url += "?" + parsed_url.query
if parsed_url.fragment:
self.url += "#" + parsed_url.fragment
if utils.recognized_git_location(self):
self.metadata_url = construct_git_url(self, "package.xml")
else:
self.metadata_url = None
@@ -334,3 +356,11 @@ class AddonManagerRepo:
for dep in self.blocks:
if dep in all_repos:
deps.blockers[dep] = all_repos[dep]
def status(self):
with self.status_lock:
return self.update_status
def set_status(self, status):
with self.status_lock:
self.update_status = status

View File

@@ -20,6 +20,7 @@ SET(AddonManager_SRCS
NetworkManager.py
package_list.py
package_details.py
loading.html
)
SOURCE_GROUP("" FILES ${AddonManager_SRCS})

View File

@@ -146,27 +146,24 @@ def get_zip_url(repo):
def recognized_git_location(repo) -> bool:
parsed_url = urlparse(repo.url)
if (
parsed_url.netloc == "github.com"
or parsed_url.netloc == "framagit.com"
or parsed_url.netloc == "gitlab.com"
or parsed_url.netloc == "salsa.debian.org"
):
if parsed_url.netloc in [
"github.com",
"framagit.org",
"gitlab.com",
"salsa.debian.org",
]:
return True
else:
return False
def construct_git_url(repo, filename):
"Returns a direct download link to a file in an online Git repo: works with github, gitlab, and framagit"
"Returns a direct download link to a file in an online Git repo: works with github, gitlab, framagit, and salsa.debian.org"
parsed_url = urlparse(repo.url)
if parsed_url.netloc == "github.com" or parsed_url.netloc == "framagit.com":
if parsed_url.netloc == "github.com":
return f"{repo.url}/raw/{repo.branch}/{filename}"
elif parsed_url.netloc == "gitlab.com":
return f"{repo.url}/-/raw/{repo.branch}/{filename}"
elif parsed_url.netloc == "salsa.debian.org":
# e.g. https://salsa.debian.org/joha2/pyrate/-/raw/master/package.xml
elif parsed_url.netloc in ["gitlab.com", "framagit.org", "salsa.debian.org"]:
return f"{repo.url}/-/raw/{repo.branch}/{filename}"
else:
FreeCAD.Console.PrintLog(
@@ -216,39 +213,12 @@ def get_readme_html_url(repo):
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
return f"{repo.url}/blob/{repo.branch}/README.md"
elif parsedUrl.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]:
return f"{repo.url}/-/blob/{repo.branch}/README.md"
else:
return None
def get_readme_regex(repo):
"""Return a regex string that extracts the contents to be displayed in the description
panel of the Addon manager, from raw HTML data (the readme's html rendering usually)"""
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
return "<article.*?>(.*?)</article>"
else:
return None
def fix_relative_links(text, base_url):
"""Replace markdown image relative links with
absolute ones using the base URL"""
new_text = ""
for line in text.splitlines():
for link in re.findall(r"!\[.*?\]\((.*?)\)", line) + re.findall(
r"src\s*=\s*[\"'](.+?)[\"']", line
):
parts = link.split("/")
if len(parts) < 2 or not re.match(r"^http|^www|^.+\.|^/", parts[0]):
newlink = os.path.join(base_url, link.lstrip("./"))
line = line.replace(link, newlink)
FreeCAD.Console.PrintLog("Debug: replaced " + link + " with " + newlink)
new_text = new_text + "\n" + line
return new_text
def repair_git_repo(repo_url: str, clone_dir: str) -> None:
# Repair addon installed with raw download by adding the .git
# directory to it

View File

@@ -359,6 +359,29 @@ class LoadMacrosFromCacheWorker(QtCore.QThread):
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: AddonManagerRepo, parent: QtCore.QObject = None):
super().__init__(parent)
self.repo = repo
def do_work(self):
# Borrow the function from another class:
checker = UpdateChecker()
if self.repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
checker.check_workbench(self.repo)
elif self.repo.repo_type == AddonManagerRepo.RepoType.MACRO:
checker.check_macro(self.repo)
elif self.repo.repo_type == AddonManagerRepo.RepoType.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"""
@@ -372,61 +395,73 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
def run(self):
if NOGIT or not have_git:
return
self.current_thread = QtCore.QThread.currentThread()
self.basedir = FreeCAD.getUserAppDataDir()
self.moddir = self.basedir + os.sep + "Mod"
checker = UpdateChecker()
count = 1
for repo in self.repos:
if self.current_thread.isInterruptionRequested():
return
self.progress_made.emit(count, len(self.repos))
count += 1
if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED:
if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
self.check_workbench(repo)
checker.check_workbench(repo)
self.update_status.emit(repo)
elif repo.repo_type == AddonManagerRepo.RepoType.MACRO:
self.check_macro(repo)
checker.check_macro(repo)
self.update_status.emit(repo)
elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
self.check_package(repo)
checker.check_package(repo)
self.update_status.emit(repo)
class UpdateChecker:
def __init__(self):
self.basedir = FreeCAD.getUserAppDataDir()
self.moddir = self.basedir + os.sep + "Mod"
def check_workbench(self, wb):
if not have_git or NOGIT:
wb.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK)
return
clonedir = self.moddir + os.sep + wb.name
if os.path.exists(clonedir):
# mark as already installed AND already checked for updates
if not os.path.exists(clonedir + os.sep + ".git"):
utils.repair_git_repo(wb.url, clonedir)
gitrepo = git.Git(clonedir)
try:
gitrepo.fetch()
except Exception:
FreeCAD.Console.PrintWarning(
"AddonManager: "
+ translate(
"AddonsInstaller",
"Unable to fetch git updates for workbench {}",
).format(wb.name)
)
else:
with wb.git_lock:
utils.repair_git_repo(wb.url, clonedir)
with wb.git_lock:
gitrepo = git.Git(clonedir)
try:
if "git pull" in gitrepo.status():
wb.update_status = (
AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE
)
else:
wb.update_status = (
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
)
self.update_status.emit(wb)
gitrepo.fetch()
except Exception:
FreeCAD.Console.PrintWarning(
translate("AddonsInstaller", "git fetch failed for {}").format(
wb.name
)
"AddonManager: "
+ translate(
"AddonsInstaller",
"Unable to fetch git updates for workbench {}",
).format(wb.name)
)
wb.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK)
else:
try:
if "git pull" in gitrepo.status():
wb.set_status(
AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE
)
else:
wb.set_status(
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
)
except Exception:
FreeCAD.Console.PrintWarning(
translate(
"AddonsInstaller", "git fetch failed for {}"
).format(wb.name)
)
wb.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK)
def check_package(self, package: AddonManagerRepo) -> None:
clonedir = self.moddir + os.sep + package.name
@@ -436,9 +471,8 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
# 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.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE
package.set_status(AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE)
package.installed_version = None
self.update_status.emit(package)
return
else:
package.updated_timestamp = os.path.getmtime(installed_metadata_file)
@@ -448,14 +482,11 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
# 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.update_status = (
AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE
)
package.set_status(AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE)
else:
package.update_status = (
package.set_status(
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
)
self.update_status.emit(package)
except Exception:
FreeCAD.Console.PrintWarning(
translate(
@@ -464,6 +495,7 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
).format(name=installed_metadata_file)
+ "\n"
)
package.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK)
def check_macro(self, macro_wrapper: AddonManagerRepo) -> None:
# Make sure this macro has its code downloaded:
@@ -486,6 +518,7 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
).format(name=macro_wrapper.macro.name)
+ "\n"
)
macro_wrapper.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK)
return
hasher1 = hashlib.sha1()
@@ -511,12 +544,9 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
else:
return
if new_sha1 == old_sha1:
macro_wrapper.update_status = (
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
)
macro_wrapper.set_status(AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE)
else:
macro_wrapper.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE
self.update_status.emit(macro_wrapper)
macro_wrapper.set_status(AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE)
class FillMacroListWorker(QtCore.QThread):
@@ -801,189 +831,6 @@ class CacheMacroCode(QtCore.QThread):
self.failed.append(macro_name)
class ShowWorker(QtCore.QThread):
"""This worker retrieves info of a given workbench"""
status_message = QtCore.Signal(str)
readme_updated = QtCore.Signal(str)
update_status = QtCore.Signal(AddonManagerRepo)
def __init__(self, repo, cache_path):
QtCore.QThread.__init__(self)
self.repo = repo
self.cache_path = cache_path
def run(self):
self.status_message.emit(
translate("AddonsInstaller", "Retrieving description...")
)
u = None
url = self.repo.url
self.status_message.emit(
translate("AddonsInstaller", "Retrieving info from {}").format(str(url))
)
desc = ""
regex = utils.get_readme_regex(self.repo)
if regex:
# extract readme from html via regex
readmeurl = utils.get_readme_html_url(self.repo)
if not readmeurl:
FreeCAD.Console.PrintLog(f"README not found for {url}\n")
p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(readmeurl)
if not p:
FreeCAD.Console.PrintLog(f"Debug: README not found at {readmeurl}\n")
p = p.data().decode("utf8")
readme = re.findall(regex, p, flags=re.MULTILINE | re.DOTALL)
if readme:
desc = readme[0]
else:
FreeCAD.Console.PrintLog(f"Debug: README not found at {readmeurl}\n")
else:
# convert raw markdown using lib
readmeurl = utils.get_readme_url(self.repo)
if not readmeurl:
FreeCAD.Console.PrintLog(f"Debug: README not found for {url}\n")
p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(readmeurl)
if p:
p = p.data().decode("utf8")
desc = utils.fix_relative_links(p, readmeurl.rsplit("/README.md")[0])
if not NOMARKDOWN and have_markdown:
desc = markdown.markdown(desc, extensions=["md_in_html"])
else:
message = """
<div style="width: 100%; text-align:center;background: #91bbe0;">
<strong style="color: #FFFFFF;">
"""
message += translate("AddonsInstaller", "Raw markdown displayed")
message += "</strong><br/><br/>"
message += translate(
"AddonsInstaller", "Python Markdown library is missing."
)
message += "<br/></div><hr/><pre>" + desc + "</pre>"
desc = message
else:
FreeCAD.Console.PrintLog("Debug: README not found at {readmeurl}\n")
if desc == "":
# fall back to the description text
p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url)
if not p:
return
p = p.data().decode("utf8")
descregex = utils.get_desc_regex(self.repo)
if descregex:
desc = re.findall(descregex, p)
if desc:
desc = desc[0]
if not desc:
desc = "Unable to retrieve addon description"
self.repo.description = desc
if QtCore.QThread.currentThread().isInterruptionRequested():
return
message = desc
if self.repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
# Addon is installed but we haven't checked it yet, so let's check if it has an update
upd = False
# checking for updates
if not NOGIT and have_git:
repo = self.repo
clonedir = (
FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + repo.name
)
if os.path.exists(clonedir):
if not os.path.exists(clonedir + os.sep + ".git"):
utils.repair_git_repo(self.repo.url, clonedir)
gitrepo = git.Git(clonedir)
gitrepo.fetch()
if "git pull" in gitrepo.status():
upd = True
if upd:
self.repo.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE
else:
self.repo.update_status = (
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
)
self.update_status.emit(self.repo)
if QtCore.QThread.currentThread().isInterruptionRequested():
return
self.readme_updated.emit(message)
self.mustLoadImages = True
label = self.loadImages(message, self.repo.url, self.repo.name)
if label:
self.readme_updated.emit(label)
if QtCore.QThread.currentThread().isInterruptionRequested():
return
def stopImageLoading(self):
"this stops the image loading process and allow the thread to terminate earlier"
self.mustLoadImages = False
def loadImages(self, message, url, wbName):
"checks if the given page contains images and downloads them"
# QTextBrowser cannot display online images. So we download them
# here, and replace the image link in the html code with the
# downloaded version
imagepaths = re.findall('<img.*?src="(.*?)"', message)
if imagepaths:
store = os.path.join(self.cache_path, "Images")
if not os.path.exists(store):
os.makedirs(store)
with open(os.path.join(store, "download_in_progress"), "w") as f:
f.write(
"If this file still exists, it's because a download was interrupted. It can be safely ignored."
)
for path in imagepaths:
if QtCore.QThread.currentThread().isInterruptionRequested():
return message
if not self.mustLoadImages:
return message
origpath = path
if "?" in path:
# remove everything after the ?
path = path.split("?")[0]
if not path.startswith("http"):
path = utils.getserver(url) + path
name = path.split("/")[-1]
if name and path.startswith("http"):
storename = os.path.join(store, name)
if len(storename) >= 260:
remainChars = 259 - (len(store) + len(wbName) + 1)
storename = os.path.join(store, wbName + name[-remainChars:])
if not os.path.exists(storename):
try:
imagedata = NetworkManager.AM_NETWORK_MANAGER.blocking_get(
path
)
if not imagedata:
raise Exception
except Exception:
FreeCAD.Console.PrintLog(
"AddonManager: Debug: Error retrieving image from "
+ path
)
else:
try:
f = open(storename, "wb")
except OSError:
# ecryptfs (and probably not only ecryptfs) has
# lower length limit for path
storename = storename[-140:]
f = open(storename, "wb")
f.write(imagedata.data())
f.close()
message = message.replace(
'src="' + origpath,
'src="file:///' + storename.replace("\\", "/"),
)
os.remove(os.path.join(store, "download_in_progress"))
return message
return None
class GetMacroDetailsWorker(QtCore.QThread):
"""Retrieve the macro details for a macro"""
@@ -1126,33 +973,34 @@ class InstallWorkbenchWorker(QtCore.QThread):
+ str(self.repo.name)
+ "\n"
)
if not os.path.exists(clonedir + os.sep + ".git"):
utils.repair_git_repo(self.repo.url, clonedir)
repo = git.Git(clonedir)
try:
repo.pull() # Refuses to take a progress object?
answer = translate(
"AddonsInstaller",
"Workbench successfully updated. Please restart FreeCAD to apply the changes.",
)
except Exception as e:
answer = (
translate("AddonsInstaller", "Error updating module ")
+ self.repo.name
+ " - "
+ translate("AddonsInstaller", "Please fix manually")
+ " -- \n"
)
answer += str(e)
self.failure.emit(self.repo, answer)
else:
# Update the submodules for this repository
repo_sms = git.Repo(clonedir)
self.status_message.emit("Updating submodules...")
for submodule in repo_sms.submodules:
submodule.update(init=True, recursive=True)
self.update_metadata()
self.success.emit(self.repo, answer)
with self.repo.git_lock:
if not os.path.exists(clonedir + os.sep + ".git"):
utils.repair_git_repo(self.repo.url, clonedir)
repo = git.Git(clonedir)
try:
repo.pull() # Refuses to take a progress object?
answer = translate(
"AddonsInstaller",
"Workbench successfully updated. Please restart FreeCAD to apply the changes.",
)
except Exception as e:
answer = (
translate("AddonsInstaller", "Error updating module ")
+ self.repo.name
+ " - "
+ translate("AddonsInstaller", "Please fix manually")
+ " -- \n"
)
answer += str(e)
self.failure.emit(self.repo, answer)
else:
# Update the submodules for this repository
repo_sms = git.Repo(clonedir)
self.status_message.emit("Updating submodules...")
for submodule in repo_sms.submodules:
submodule.update(init=True, recursive=True)
self.update_metadata()
self.success.emit(self.repo, answer)
def run_git_clone(self, clonedir: str) -> None:
self.status_message.emit("Checking module dependencies...")
@@ -1168,21 +1016,39 @@ class InstallWorkbenchWorker(QtCore.QThread):
self.status_message.emit("Cloning module...")
current_thread = QtCore.QThread.currentThread()
# NOTE: There is no way to interrupt this process in GitPython: someday we should
# support pygit2/libgit2 so we can actually interrupt this properly.
repo = git.Repo.clone_from(self.repo.url, clonedir, progress=self.git_progress)
if current_thread.isInterruptionRequested():
return
FreeCAD.Console.PrintMessage("Cloning repo...\n")
if self.repo.git_lock.locked():
FreeCAD.Console.PrintMessage("Waiting for lock to be released to us...\n")
if not self.repo.git_lock.acquire(timeout=2):
FreeCAD.Console.PrintError("Timeout waiting for a lock on the git process, failed to clone repo\n")
return
else:
self.repo.git_lock.release()
# Make sure to clone all the submodules as well
if repo.submodules:
repo.submodule_update(recursive=True)
with self.repo.git_lock:
FreeCAD.Console.PrintMessage("Lock acquired...\n")
# NOTE: There is no way to interrupt this process in GitPython: someday we should
# support pygit2/libgit2 so we can actually interrupt this properly.
repo = git.Repo.clone_from(
self.repo.url, clonedir, progress=self.git_progress
)
FreeCAD.Console.PrintMessage("Initial clone complete...\n")
if current_thread.isInterruptionRequested():
return
if current_thread.isInterruptionRequested():
return
# Make sure to clone all the submodules as well
if repo.submodules:
FreeCAD.Console.PrintMessage("Updating submodules...\n")
repo.submodule_update(recursive=True)
if self.repo.branch in repo.heads:
repo.heads[self.repo.branch].checkout()
if current_thread.isInterruptionRequested():
return
if self.repo.branch in repo.heads:
FreeCAD.Console.PrintMessage("Checking out HEAD...\n")
repo.heads[self.repo.branch].checkout()
FreeCAD.Console.PrintMessage("Clone complete\n")
answer = translate(
"AddonsInstaller",
@@ -1465,38 +1331,6 @@ class DependencyInstallationWorker(QtCore.QThread):
self.success.emit()
class CheckSingleWorker(QtCore.QThread):
"""Worker to check for updates for a single addon"""
updateAvailable = QtCore.Signal(bool)
def __init__(self, name):
QtCore.QThread.__init__(self)
self.name = name
def run(self):
if not have_git or NOGIT:
return
FreeCAD.Console.PrintLog(
"Checking for available updates of the " + self.name + " addon\n"
)
addondir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", self.name)
if os.path.exists(addondir):
if os.path.exists(addondir + os.sep + ".git"):
gitrepo = git.Git(addondir)
try:
gitrepo.fetch()
if "git pull" in gitrepo.status():
self.updateAvailable.emit(True)
return
except Exception:
# can fail for any number of reasons, ex. not being online
pass
self.updateAvailable.emit(False)
class UpdateMetadataCacheWorker(QtCore.QThread):
"Scan through all available packages and see if our local copy of package.xml needs to be updated"

View File

@@ -0,0 +1,93 @@
<!--
Adapted from:
https://codepen.io/MattIn4D/pen/LiKFC
Copyright © 2021 MattIn4D
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<html>
<head>
<style>
/* Absolute Center Spinner */
.loading {
position: fixed;
z-index: 999;
height: 2em;
width: 2em;
overflow: visible;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
/* Transparent Overlay */
.loading:before {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.3);
}
/* :not(:required) hides these rules from IE9 and below */
.loading:not(:required) {
/* hide "loading..." text */
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.loading:not(:required):after {
content: '';
display: block;
font-size: 10px;
width: 1em;
height: 1em;
margin-top: -0.5em;
animation: spinner 5000ms infinite linear;
border-radius: 0.5em;
box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) -1.5em 0 0 0, rgba(0, 0, 0, 0.75) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
}
/* Animation */
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="loading">Loading...</div>
</body>
</html>

View File

@@ -24,21 +24,20 @@
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from PySide2.QtWebEngineWidgets import *
import os
import shutil
from datetime import date, timedelta
import FreeCAD
import addonmanager_utilities as utils
from addonmanager_workers import ShowWorker, GetMacroDetailsWorker
from addonmanager_workers import GetMacroDetailsWorker, CheckSingleUpdateWorker
from AddonManagerRepo import AddonManagerRepo
import inspect
translate = FreeCAD.Qt.translate
show_javascript_console_output = False
class PackageDetails(QWidget):
@@ -57,9 +56,10 @@ class PackageDetails(QWidget):
self.worker = None
self.repo = None
self.status_update_thread = None
self.ui.buttonBack.clicked.connect(self.back.emit)
self.ui.buttonRefresh.clicked.connect(self.refresh)
self.ui.buttonBack.clicked.connect(self.clear_web_view)
self.ui.buttonExecute.clicked.connect(lambda: self.execute.emit(self.repo))
self.ui.buttonInstall.clicked.connect(lambda: self.install.emit(self.repo))
self.ui.buttonUninstall.clicked.connect(lambda: self.uninstall.emit(self.repo))
@@ -67,34 +67,64 @@ class PackageDetails(QWidget):
self.ui.buttonCheckForUpdate.clicked.connect(
lambda: self.check_for_update.emit(self.repo)
)
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") as f:
html = f.read()
self.ui.loadingLabel.setHtml(html)
self.ui.loadingLabel.show()
self.ui.webView.hide()
def show_repo(self, repo: AddonManagerRepo, reload: bool = False) -> None:
self.repo = repo
# If this is the same repo we were already showing, we do not have to do the
# expensive refetch unless reload is true
if self.repo != repo or reload:
self.repo = repo
self.ui.loadingLabel.show()
self.ui.webView.hide()
self.ui.progressBar.show()
if self.worker is not None:
if not self.worker.isFinished():
self.worker.requestInterruption()
self.worker.wait()
if self.worker is not None:
if not self.worker.isFinished():
self.worker.requestInterruption()
self.worker.wait()
# Always load bare macros from scratch, we need to grab their code, which isn't cached
force_reload = reload
if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
force_reload = True
if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
self.show_macro(repo)
self.ui.buttonExecute.show()
elif repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
self.show_workbench(repo)
self.ui.buttonExecute.hide()
elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
self.show_package(repo)
self.ui.buttonExecute.hide()
self.check_and_clean_cache(force_reload)
if self.status_update_thread is not None:
if not self.status_update_thread.isFinished():
self.status_update_thread.requestInterruption()
self.status_update_thread.wait()
if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
self.show_macro(repo)
self.ui.buttonExecute.show()
elif repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
self.show_workbench(repo)
self.ui.buttonExecute.hide()
elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
self.show_package(repo)
self.ui.buttonExecute.hide()
if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED:
self.status_update_thread = QThread()
self.status_update_worker = CheckSingleUpdateWorker(repo, self)
self.status_update_worker.moveToThread(self.status_update_thread)
self.status_update_thread.finished.connect(
self.status_update_worker.deleteLater
)
self.check_for_update.connect(self.status_update_worker.do_work)
self.status_update_worker.update_status.connect(self.display_repo_status)
self.status_update_thread.start()
self.check_for_update.emit(self.repo)
if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
self.display_repo_status(self.repo.update_status)
def display_repo_status(self, status):
repo = self.repo
if status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
version = repo.installed_version
date = ""
@@ -125,7 +155,7 @@ class PackageDetails(QWidget):
translate("AddonsInstaller", "Installed") + ". "
)
if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
if status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
if repo.metadata:
installed_version_string += (
"<b>"
@@ -151,21 +181,19 @@ class PackageDetails(QWidget):
)
+ ".</b>"
)
elif (
repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
):
elif status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
installed_version_string += (
translate("AddonsInstaller", "This is the latest version available")
+ "."
)
elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
elif status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
installed_version_string += (
translate(
"AddonsInstaller", "Updated, please restart FreeCAD to use"
)
+ "."
)
elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
elif status == AddonManagerRepo.UpdateStatus.UNCHECKED:
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
autocheck = pref.GetBool("AutoCheck", False)
@@ -181,7 +209,7 @@ class PackageDetails(QWidget):
installed_version_string += "</h3>"
self.ui.labelPackageDetails.setText(installed_version_string)
if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
if repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
self.ui.labelPackageDetails.setStyleSheet(
"color:" + utils.attention_color_string()
)
@@ -206,27 +234,27 @@ class PackageDetails(QWidget):
self.ui.labelPackageDetails.hide()
self.ui.labelInstallationLocation.hide()
if repo.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
if status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
self.ui.buttonInstall.show()
self.ui.buttonUninstall.hide()
self.ui.buttonUpdate.hide()
self.ui.buttonCheckForUpdate.hide()
elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
elif status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
self.ui.buttonInstall.hide()
self.ui.buttonUninstall.show()
self.ui.buttonUpdate.hide()
self.ui.buttonCheckForUpdate.hide()
elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
elif status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
self.ui.buttonInstall.hide()
self.ui.buttonUninstall.show()
self.ui.buttonUpdate.show()
self.ui.buttonCheckForUpdate.hide()
elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
elif status == AddonManagerRepo.UpdateStatus.UNCHECKED:
self.ui.buttonInstall.hide()
self.ui.buttonUninstall.show()
self.ui.buttonUpdate.hide()
self.ui.buttonCheckForUpdate.show()
elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
elif status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
self.ui.buttonInstall.hide()
self.ui.buttonUninstall.show()
self.ui.buttonUpdate.hide()
@@ -255,127 +283,155 @@ class PackageDetails(QWidget):
else:
self.ui.labelWarningInfo.hide()
@classmethod
def cache_path(self, repo: AddonManagerRepo) -> str:
cache_path = FreeCAD.getUserCachePath()
full_path = os.path.join(cache_path, "AddonManager", repo.name)
return full_path
def check_and_clean_cache(self, force: bool = False) -> None:
cache_path = PackageDetails.cache_path(self.repo)
readme_cache_file = os.path.join(cache_path, "README.html")
readme_images_path = os.path.join(cache_path, "Images")
download_interrupted_sentinel = os.path.join(
readme_images_path, "download_in_progress"
)
download_interrupted = os.path.isfile(download_interrupted_sentinel)
if os.path.isfile(readme_cache_file):
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
days_between_updates = pref.GetInt("DaysBetweenUpdates", 2 ^ 32)
timestamp = os.path.getmtime(readme_cache_file)
last_cache_update = date.fromtimestamp(timestamp)
delta_update = timedelta(days=days_between_updates)
if (
date.today() >= last_cache_update + delta_update
or download_interrupted
or force
):
if force:
FreeCAD.Console.PrintLog(
f"Forced README cache update for {self.repo.name}\n"
)
elif download_interrupted:
FreeCAD.Console.PrintLog(
f"Restarting interrupted README download for {self.repo.name}\n"
)
else:
FreeCAD.Console.PrintLog(
f"Cache expired, downloading README for {self.repo.name} again\n"
)
os.remove(readme_cache_file)
if os.path.isdir(readme_images_path):
shutil.rmtree(readme_images_path)
def refresh(self):
self.check_and_clean_cache(force=True)
self.show_repo(self.repo)
def show_cached_readme(self, repo: AddonManagerRepo) -> bool:
"""Attempts to show a cached readme, returns true if there was a cache, or false if not"""
cache_path = PackageDetails.cache_path(repo)
readme_cache_file = os.path.join(cache_path, "README.html")
if os.path.isfile(readme_cache_file):
with open(readme_cache_file, "rb") as f:
data = f.read()
self.ui.textBrowserReadMe.setText(data.decode())
return True
return False
def show_workbench(self, repo: AddonManagerRepo) -> None:
"""loads information of a given workbench"""
if not self.show_cached_readme(repo):
self.ui.textBrowserReadMe.setText(
translate(
"AddonsInstaller", "Fetching README.md from package repository"
)
)
self.worker = ShowWorker(repo, PackageDetails.cache_path(repo))
self.worker.readme_updated.connect(
lambda desc: self.cache_readme(repo, desc)
)
self.worker.readme_updated.connect(
lambda desc: self.ui.textBrowserReadMe.setText(desc)
)
self.worker.update_status.connect(self.update_status.emit)
self.worker.update_status.connect(self.show)
self.worker.start()
url = utils.get_readme_html_url(repo)
self.ui.webView.load(QUrl(url))
self.ui.urlBar.setText(url)
def show_package(self, repo: AddonManagerRepo) -> None:
"""Show the details for a package (a repo with a package.xml metadata file)"""
if not self.show_cached_readme(repo):
self.ui.textBrowserReadMe.setText(
translate(
"AddonsInstaller", "Fetching README.md from package repository"
)
)
self.worker = ShowWorker(repo, PackageDetails.cache_path(repo))
self.worker.readme_updated.connect(
lambda desc: self.cache_readme(repo, desc)
)
self.worker.readme_updated.connect(
lambda desc: self.ui.textBrowserReadMe.setText(desc)
)
self.worker.update_status.connect(self.update_status.emit)
self.worker.update_status.connect(self.show)
self.worker.start()
readme_url = None
if repo.metadata:
urls = repo.metadata.Urls
for url in urls:
if url["type"] == "readme":
readme_url = url["location"]
break
if not readme_url:
readme_url = utils.get_readme_html_url(repo)
self.ui.webView.load(QUrl(readme_url))
self.ui.urlBar.setText(readme_url)
def show_macro(self, repo: AddonManagerRepo) -> None:
"""loads information of a given macro"""
if not self.show_cached_readme(repo):
self.ui.textBrowserReadMe.setText(
translate(
"AddonsInstaller", "Fetching README.md from package repository"
)
)
self.worker = GetMacroDetailsWorker(repo)
self.worker.readme_updated.connect(
lambda desc: self.cache_readme(repo, desc)
)
self.worker.readme_updated.connect(
lambda desc: self.ui.textBrowserReadMe.setText(desc)
)
self.worker.start()
self.ui.webView.load(QUrl(repo.macro.url))
self.ui.urlBar.setText(repo.macro.url)
def cache_readme(self, repo: AddonManagerRepo, readme: str) -> None:
cache_path = PackageDetails.cache_path(repo)
readme_cache_file = os.path.join(cache_path, "README.html")
os.makedirs(cache_path, exist_ok=True)
with open(readme_cache_file, "wb") as f:
f.write(readme.encode())
# 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.start()
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.freecadweb.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 clear_web_view(self):
self.ui.webView.setHtml("<html><body><h1>Loading...</h1></body></html>")
def load_started(self):
self.ui.progressBar.show()
self.ui.progressBar.setValue(0)
def load_progress(self, progress: int):
self.ui.progressBar.setValue(progress)
def load_finished(self, load_succeeded: bool):
self.ui.loadingLabel.hide()
self.ui.webView.show()
self.ui.progressBar.hide()
url = self.ui.webView.url()
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 show_error_for(self, url: QUrl) -> None:
m = translate(
"AddonsInstaller", "Could not load README data from URL {}"
).format(url.toString())
html = f"<html><body><p>{m}</p></body></html>"
self.ui.webView.setHtml(html)
class RestrictedWebPage(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(QWebEngineSettings.ErrorPageEnabled, False)
def acceptNavigationRequest(self, url, _type, isMainFrame):
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
if url.host() == "wiki.freecad.org" or url.host() == "wiki.freecadweb.org":
return super().acceptNavigationRequest(url, _type, isMainFrame)
else:
QDesktopServices.openUrl(url)
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID):
global show_javascript_console_output
if show_javascript_console_output:
tag = translate("AddonsInstaller", "Page JavaScript reported")
if level == QWebEnginePage.InfoMessageLevel:
FreeCAD.Console.PrintMessage(f"{tag} {lineNumber}: {message}\n")
elif level == QWebEnginePage.WarningMessageLevel:
FreeCAD.Console.PrintWarning(f"{tag} {lineNumber}: {message}\n")
elif level == QWebEnginePage.ErrorMessageLevel:
FreeCAD.Console.PrintError(f"{tag} {lineNumber}: {message}\n")
class Ui_PackageDetails(object):
@@ -391,14 +447,8 @@ class Ui_PackageDetails(object):
self.buttonBack.setIcon(
QIcon.fromTheme("back", QIcon(":/icons/button_left.svg"))
)
self.buttonRefresh = QToolButton(PackageDetails)
self.buttonRefresh.setObjectName("buttonRefresh")
self.buttonRefresh.setIcon(
QIcon.fromTheme("refresh", QIcon(":/icons/view-refresh.svg"))
)
self.layoutDetailsBackButton.addWidget(self.buttonBack)
self.layoutDetailsBackButton.addWidget(self.buttonRefresh)
self.horizontalSpacer = QSpacerItem(
40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum
@@ -449,12 +499,34 @@ class Ui_PackageDetails(object):
self.verticalLayout_2.addWidget(self.labelWarningInfo)
self.textBrowserReadMe = QTextBrowser(PackageDetails)
self.textBrowserReadMe.setObjectName("textBrowserReadMe")
self.textBrowserReadMe.setOpenExternalLinks(True)
self.textBrowserReadMe.setOpenLinks(True)
sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
self.verticalLayout_2.addWidget(self.textBrowserReadMe)
self.webView = QWebEngineView(PackageDetails)
self.webView.setObjectName("webView")
self.webView.setSizePolicy(sizePolicy1)
self.webView.setPage(RestrictedWebPage(PackageDetails))
self.verticalLayout_2.addWidget(self.webView)
self.loadingLabel = QWebEngineView(PackageDetails)
self.loadingLabel.setObjectName("loadingLabel")
self.loadingLabel.setSizePolicy(sizePolicy1)
self.verticalLayout_2.addWidget(self.loadingLabel)
self.progressBar = QProgressBar(PackageDetails)
self.progressBar.setObjectName("progressBar")
self.progressBar.setTextVisible(False)
self.verticalLayout_2.addWidget(self.progressBar)
self.urlBar = QLineEdit(PackageDetails)
self.urlBar.setObjectName("urlBar")
self.urlBar.setReadOnly(True)
self.verticalLayout_2.addWidget(self.urlBar)
self.retranslateUi(PackageDetails)
@@ -462,7 +534,7 @@ class Ui_PackageDetails(object):
# setupUi
def retranslateUi(self, PackageDetails):
def retranslateUi(self, _):
self.buttonBack.setText("")
self.buttonInstall.setText(
QCoreApplication.translate("AddonsInstaller", "Install", None)
@@ -484,12 +556,5 @@ class Ui_PackageDetails(object):
"AddonsInstaller", "Return to package list", None
)
)
self.buttonRefresh.setToolTip(
QCoreApplication.translate(
"AddonsInstaller",
"Delete cached version of this README and re-download",
None,
)
)
# retranslateUi

View File

@@ -226,7 +226,7 @@ class PackageListItemModel(QAbstractListModel):
row = index.row()
self.write_lock.acquire()
if role == PackageListItemModel.StatusUpdateRole:
self.repos[row].update_status = value
self.repos[row].set_status(value)
self.dataChanged.emit(
self.index(row, 2),
self.index(row, 2),
@@ -408,13 +408,13 @@ class PackageListItemDelegate(QStyledItemDelegate):
"""Get a single-line string listing details about the installed version and date"""
result = ""
if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED:
result = translate("AddonsInstaller", "Installed")
elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
elif repo.status() == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
result = translate("AddonsInstaller", "Up-to-date")
elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
elif repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
result = translate("AddonsInstaller", "Update available")
elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
elif repo.status() == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
result = translate("AddonsInstaller", "Pending restart")
return result
@@ -424,7 +424,7 @@ class PackageListItemDelegate(QStyledItemDelegate):
result = ""
installed_version_string = ""
if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
if repo.status() != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
if repo.installed_version:
installed_version_string = (
"\n" + translate("AddonsInstaller", "Installed version") + ": "
@@ -453,20 +453,20 @@ class PackageListItemDelegate(QStyledItemDelegate):
)
available_version_string += repo.metadata.Version
if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED:
result = translate("AddonsInstaller", "Installed")
result += installed_version_string
result += installed_date_string
elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
elif repo.status() == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
result = translate("AddonsInstaller", "Up-to-date")
result += installed_version_string
result += installed_date_string
elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
elif repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
result = translate("AddonsInstaller", "Update available")
result += installed_version_string
result += installed_date_string
result += available_version_string
elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
elif repo.status() == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
result = translate("AddonsInstaller", "Pending restart")
return result
@@ -530,18 +530,18 @@ class PackageListFilter(QSortFilterProxyModel):
return False
if self.status == StatusFilter.INSTALLED:
if data.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
if data.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
return False
elif self.status == StatusFilter.NOT_INSTALLED:
if data.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
if data.status() != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
return False
elif self.status == StatusFilter.UPDATE_AVAILABLE:
if data.update_status != AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
if data.status() != AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
return False
# If it's not installed, check to see if it's Py2 only
if (
data.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED
data.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED
and self.hide_py2
and data.python2
):
@@ -549,7 +549,7 @@ class PackageListFilter(QSortFilterProxyModel):
# If it's not installed, check to see if it's marked obsolete
if (
data.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED
data.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED
and self.hide_obsolete
and data.obsolete
):