From 94898424d5ed4a08128b6abc9bd520c3b491514a Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Wed, 13 Aug 2025 19:40:04 -0500 Subject: [PATCH] Stop attempting to display the Macro wiki page This was error-prone and fragile, and prevented the wiki from being reformatted because it would break the AM's display. Switch to just showing the macros metadata, and also add display of the code (some macros include useful information in their code's header). --- AddonManager.py | 4 +- Widgets/addonmanager_widget_addon_buttons.py | 10 +- addonmanager_package_details_controller.py | 78 +++--- addonmanager_readme_controller.py | 249 ++++++------------- composite_view.py | 2 +- 5 files changed, 135 insertions(+), 208 deletions(-) diff --git a/AddonManager.py b/AddonManager.py index abd950ce..f267e458 100644 --- a/AddonManager.py +++ b/AddonManager.py @@ -620,7 +620,7 @@ def mark_repo_update_available(self, repo: Addon, available: bool) -> None: else: repo.set_status(Addon.Status.NO_UPDATE_AVAILABLE) self.item_model.reload_item(repo) - self.composite_view.package_details_controller.show_repo(repo) + self.composite_view.package_details_controller.show_addon(repo) def prep_for_install(self, installing_addon: Addon): """To prepare for installing an addon, we need to see if this is the current active branch: @@ -712,7 +712,7 @@ def on_package_status_changed(self, repo: Addon) -> None: if repo.status() == Addon.Status.PENDING_RESTART: self.restart_required = True self.item_model.reload_item(repo) - self.composite_view.package_details_controller.show_repo(repo) + self.composite_view.package_details_controller.show_addon(repo) if repo in self.packages_with_updates: self.packages_with_updates.remove(repo) self.enable_updates(len(self.packages_with_updates)) diff --git a/Widgets/addonmanager_widget_addon_buttons.py b/Widgets/addonmanager_widget_addon_buttons.py index dc487b6e..4b75c931 100644 --- a/Widgets/addonmanager_widget_addon_buttons.py +++ b/Widgets/addonmanager_widget_addon_buttons.py @@ -67,7 +67,11 @@ def set_can_check_for_updates(self, can_check_for_updates: bool): self.update.setVisible(can_check_for_updates) def set_installation_status( - self, installed: bool, available_branches: List[str], disabled: bool + self, + installed: bool, + available_branches: List[str], + disabled: bool, + can_be_disabled: bool = True, ): """Set up the buttons for a given installation status. :param installed: Whether the addon is currently installed or not. @@ -100,8 +104,12 @@ def set_installation_status( self.actions.append(new_action) self.branch_menu.addAction(new_action) + if can_be_disabled: self.enable.setVisible(installed and disabled) self.disable.setVisible(installed and not disabled) + else: + self.enable.setVisible(False) + self.disable.setVisible(False) self.retranslateUi(None) def action_activated(self, _): diff --git a/addonmanager_package_details_controller.py b/addonmanager_package_details_controller.py index 58ec2718..9ea1abba 100644 --- a/addonmanager_package_details_controller.py +++ b/addonmanager_package_details_controller.py @@ -24,15 +24,12 @@ """Provides the PackageDetails widget.""" import os -from typing import Optional from PySideWrapper import QtCore, QtWidgets import addonmanager_freecad_interface as fci from addonmanager_metadata import ( - Version, - get_first_supported_freecad_version, get_branch_from_metadata, get_repo_url_from_metadata, ) @@ -79,7 +76,7 @@ def __init__(self, widget=None): self.ui.button_bar.disable.clicked.connect(self.disable_clicked) self.ui.button_bar.install_branch.connect(self.install_branch) - def show_repo(self, addon: Addon) -> None: + def show_addon(self, addon: Addon) -> None: """The main entry point for this class shows the package details and related buttons for the provided repo.""" self.addon = addon @@ -87,23 +84,15 @@ def show_repo(self, addon: Addon) -> None: self.original_disabled_state = self.addon.is_disabled() if addon is not None: self.ui.button_bar.show() - branches = [] - if addon.status() == Addon.Status.NOT_INSTALLED: - branches.append(addon.branch_display_name) - if addon.sub_addons: - branches.extend(addon.sub_addons.keys()) - self.ui.button_bar.set_installation_status( - installed=addon.status() != Addon.Status.NOT_INSTALLED, - available_branches=branches, - disabled=addon.is_disabled(), - ) - self.ui.button_bar.set_can_run(addon.contains_macro()) - self.ui.button_bar.set_can_check_for_updates(True if addon.cache_directory else False) - if addon.name == "AddonManager": - self.ui.button_bar.setup_for_addon_manager() # Must happen AFTER other config steps + if addon.repo_type == Addon.Kind.MACRO: + self.set_up_macro_display() + else: + self.set_up_non_macro_display() + self.set_up_updater() else: self.ui.button_bar.hide() + def set_up_updater(self): if self.worker is not None: if not self.worker.isFinished(): self.worker.requestInterruption() @@ -111,8 +100,8 @@ def show_repo(self, addon: Addon) -> None: installed = self.addon.status() != Addon.Status.NOT_INSTALLED self.ui.set_installed(installed) - if addon.metadata is not None: - self.ui.set_url(get_repo_url_from_metadata(addon.metadata)) + if self.addon.metadata is not None: + self.ui.set_url(get_repo_url_from_metadata(self.addon.metadata)) else: self.ui.set_url(None) # to reset it and hide it update_info = UpdateInformation() @@ -120,28 +109,26 @@ def show_repo(self, addon: Addon) -> None: update_info.unchecked = self.addon.status() == Addon.Status.UNCHECKED update_info.update_available = self.addon.status() == Addon.Status.UPDATE_AVAILABLE update_info.check_in_progress = False # TODO: Implement the "check in progress" status - if addon.metadata: - update_info.branch = get_branch_from_metadata(addon.metadata) - update_info.version = str(addon.metadata.version) - elif addon.macro: - update_info.version = str(addon.macro.version) + if self.addon.metadata: + update_info.branch = get_branch_from_metadata(self.addon.metadata) + update_info.version = str(self.addon.metadata.version) + elif self.addon.macro: + update_info.version = str(self.addon.macro.version) self.ui.set_update_available(update_info) self.ui.set_location( self.addon.macro_directory - if addon.repo_type == Addon.Kind.MACRO + if self.addon.repo_type == Addon.Kind.MACRO else os.path.join(self.addon.mod_directory, self.addon.name) ) self.ui.set_disabled(self.addon.is_disabled()) - self.ui.allow_running(addon.repo_type == Addon.Kind.MACRO) - self.ui.allow_disabling(addon.repo_type != Addon.Kind.MACRO) - if addon.status() == Addon.Status.UNCHECKED: + if self.addon.status() == Addon.Status.UNCHECKED: if not self.update_check_thread: self.update_check_thread = QtCore.QThread() self.update_check_thread.setObjectName( "PackageDetailsController update check thread" ) - self.check_for_update_worker = CheckSingleUpdateWorker(addon) + self.check_for_update_worker = CheckSingleUpdateWorker(self.addon) self.check_for_update_worker.moveToThread(self.update_check_thread) self.update_check_thread.finished.connect(self.check_for_update_worker.deleteLater) self.ui.button_bar.check_for_update.clicked.connect( @@ -155,6 +142,32 @@ def show_repo(self, addon: Addon) -> None: flags = WarningFlags() self.ui.set_warning_flags(flags) + def set_up_non_macro_display(self): + branches = [] + if self.addon.status() == Addon.Status.NOT_INSTALLED: + branches.append(self.addon.branch_display_name) + if self.addon.sub_addons: + branches.extend(self.addon.sub_addons.keys()) + self.ui.button_bar.set_installation_status( + installed=self.addon.status() != Addon.Status.NOT_INSTALLED, + available_branches=branches, + disabled=self.addon.is_disabled(), + ) + self.ui.button_bar.set_can_run(False) + self.ui.button_bar.set_can_check_for_updates(True if self.addon.cache_directory else False) + if self.addon.name == "AddonManager": + self.ui.button_bar.setup_for_addon_manager() # Must happen AFTER other config steps + + def set_up_macro_display(self): + self.ui.button_bar.set_installation_status( + installed=self.addon.status() != Addon.Status.NOT_INSTALLED, + available_branches=[], + disabled=self.addon.is_disabled(), + can_be_disabled=False, + ) + self.ui.button_bar.set_can_run(True) + self.ui.button_bar.set_can_check_for_updates(False) + def enable_clicked(self) -> None: """Called by the Enable button, enables this Addon and updates GUI to reflect that status.""" @@ -229,7 +242,4 @@ def branch_changed(self, old_branch: str, name: str) -> None: def display_repo_status(self, addon): self.update_status.emit(self.addon) - self.show_repo(self.addon) - - def macro_readme_updated(self): - self.show_repo(self.addon) + self.show_addon(self.addon) diff --git a/addonmanager_readme_controller.py b/addonmanager_readme_controller.py index fe90623e..36bc2726 100644 --- a/addonmanager_readme_controller.py +++ b/addonmanager_readme_controller.py @@ -27,8 +27,7 @@ import addonmanager_utilities as utils import addonmanager_freecad_interface as fci -from enum import IntEnum, Enum, auto -from html.parser import HTMLParser +from enum import IntEnum from typing import Optional import NetworkManager @@ -72,61 +71,9 @@ def set_addon(self, repo: Addon): self.stop = False self.readme_data = None if self.addon.repo_type == Addon.Kind.MACRO: - self.url = self.addon.macro.wiki - if not self.url: - self.url = self.addon.macro.url - if not self.url: - self.widget.setText( - translate( - "AddonsInstaller", - "Loading info for {} from the FreeCAD Macro Recipes wiki...", - ).format(self.addon.display_name, self.url) - ) - return - else: - self.url = utils.get_readme_url(repo) - if self.addon.metadata and self.addon.metadata.url: - for url in self.addon.metadata.url: - if url.type == UrlType.readme: - if self.url != url.location: - fci.Console.PrintLog("README url does not match expected location\n") - fci.Console.PrintLog(f"Expected: {self.url}\n") - fci.Console.PrintLog(f"package.xml contents: {url.location}\n") - fci.Console.PrintLog( - "Note to addon devs: package.xml now expects a" - " url to the raw MD data, now that Qt can render" - " it without having it transformed to HTML.\n" - ) - self.url = url.location - if "/blob/" in self.url: - fci.Console.PrintLog("Attempting to replace 'blob' with 'raw'...\n") - self.url = self.url.replace("/blob/", "/raw/") - elif "/src/" in self.url and "codeberg" in self.url: - fci.Console.PrintLog( - "Attempting to replace 'src' with 'raw' in codeberg URL..." - ) - self.url = self.url.replace("/src/", "/raw/") - - self.widget.setUrl(self.url) - - self.widget.setText( - translate("AddonsInstaller", "Loading page for {} from {}...").format( - self.addon.display_name, self.url - ) - ) - - if self.url[0] == "/": - if self.url[:3] == ".md": - self.readme_data_type = ReadmeDataType.Markdown - elif self.url[:5] == ".html": - self.readme_data_type = ReadmeDataType.Html - - with open(self.url, "r") as fd: - self._process_package_download("".join(fd.readlines())) + self._create_wiki_display() else: - self.readme_request_index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get( - self.url - ) + self._create_non_wiki_display() def _download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> None: """Callback for handling a completed README file download.""" @@ -159,16 +106,9 @@ def _download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> self.set_addon(self.addon) # Trigger a reload of the page now with resources def _process_package_download(self, data: str): - if self.addon.repo_type == Addon.Kind.MACRO: - parser = WikiCleaner() - parser.feed(data) - self.readme_data = parser.final_html - self.readme_data_type = ReadmeDataType.Html - self.widget.setHtml(parser.final_html) - else: - self.readme_data = data - self.readme_data_type = ReadmeDataType.Markdown - self.widget.setMarkdown(data) + self.readme_data = data + self.readme_data_type = ReadmeDataType.Markdown + self.widget.setMarkdown(data) def _process_resource_download(self, resource_name: str, resource_data: bytes): image = QtGui.QImage.fromData(resource_data) @@ -208,112 +148,81 @@ def _create_markdown_url(self, file: str) -> str: lhs, slash, _ = base_url.rpartition("/") return lhs + slash + file + def _create_wiki_display(self): + """The Addon Manager used to parse the wiki page and display it in the Qt widget. This was + very fragile and is no longer used. Now, display the metadata we have for the wiki and + provide a link to the Wiki page for the macro, which will open in an external browser.""" + + markdown = f"# {self.addon.display_name}\n\n" + if self.addon.macro.comment: + markdown += f"{self.addon.macro.comment}\n\n" + elif self.addon.macro.desc: + markdown += f"{self.addon.macro.desc}\n\n" + if self.addon.macro.author: + markdown += f"* Author: {self.addon.macro.author}\n" + if self.addon.macro.version: + markdown += f"* Version: {self.addon.macro.version}\n" + if self.addon.macro.date: + markdown += f"* Date: {self.addon.macro.date}\n" + if self.addon.macro.license: + markdown += f"* License: {self.addon.macro.license}\n" + if self.addon.macro.url: + markdown += f"* URL: [{self.addon.macro.url}]({self.addon.macro.url})\n" + wiki_page_name = self.addon.macro.name.replace(" ", "_") + wiki_page_name = wiki_page_name.replace("&", "%26") + wiki_page_name = wiki_page_name.replace("+", "%2B") + url = "https://wiki.freecad.org/Macro_" + wiki_page_name + if url != self.addon.macro.url: + markdown += f"* Wiki page: [{url}]({url})\n" + if self.addon.macro.code: + markdown += "\n## Macro Code\n\n```python\n" + markdown += self.addon.macro.code + markdown += "\n```\n" + self.readme_data = markdown + self.readme_data_type = ReadmeDataType.Markdown + self.widget.setMarkdown(markdown) + + def _create_non_wiki_display(self): + self.url = utils.get_readme_url(self.addon) + if self.addon.metadata and self.addon.metadata.url: + for url in self.addon.metadata.url: + if url.type == UrlType.readme: + if self.url != url.location: + fci.Console.PrintLog("README url does not match expected location\n") + fci.Console.PrintLog(f"Expected: {self.url}\n") + fci.Console.PrintLog(f"package.xml contents: {url.location}\n") + fci.Console.PrintLog( + "Note to addon devs: package.xml now expects a" + " url to the raw MD data, now that Qt can render" + " it without having it transformed to HTML.\n" + ) + self.url = url.location + if "/blob/" in self.url: + fci.Console.PrintLog("Attempting to replace 'blob' with 'raw'...\n") + self.url = self.url.replace("/blob/", "/raw/") + elif "/src/" in self.url and "codeberg" in self.url: + fci.Console.PrintLog( + "Attempting to replace 'src' with 'raw' in codeberg URL..." + ) + self.url = self.url.replace("/src/", "/raw/") -class WikiCleaner(HTMLParser): - """This HTML parser cleans up FreeCAD Macro Wiki Page for display in a - QTextBrowser widget (which does not deal will with tables used as formatting, - etc.) It strips out any tables, and extracts the mw-parser-output div as the only - thing that actually gets displayed. It also discards anything inside the [edit] - spans that litter wiki output.""" - - class State(Enum): - BeforeMacroContent = auto() - InMacroContent = auto() - InTable = auto() - InEditSpan = auto() - AfterMacroContent = auto() - - def __init__(self): - super().__init__() - self.depth_in_div = 0 - self.depth_in_span = 0 - self.depth_in_table = 0 - self.final_html = "" - self.previous_state = WikiCleaner.State.BeforeMacroContent - self.state = WikiCleaner.State.BeforeMacroContent - - def handle_starttag(self, tag: str, attrs): - if tag == "div": - self.handle_div_start(attrs) - elif tag == "span": - self.handle_span_start(attrs) - elif tag == "table": - self.handle_table_start(attrs) - else: - if self.state == WikiCleaner.State.InMacroContent: - self.add_tag_to_html(tag, attrs) - - def handle_div_start(self, attrs): - for name, value in attrs: - if name == "class" and value == "mw-parser-output": - self.previous_state = self.state - self.state = WikiCleaner.State.InMacroContent - if self.state == WikiCleaner.State.InMacroContent: - self.depth_in_div += 1 - self.add_tag_to_html("div", attrs) - - def handle_span_start(self, attrs): - for name, value in attrs: - if name == "class" and value == "mw-editsection": - self.previous_state = self.state - self.state = WikiCleaner.State.InEditSpan - break - if self.state == WikiCleaner.State.InEditSpan: - self.depth_in_span += 1 - elif WikiCleaner.State.InMacroContent: - self.add_tag_to_html("span", attrs) - - def handle_table_start(self, unused): - if self.state != WikiCleaner.State.InTable: - self.previous_state = self.state - self.state = WikiCleaner.State.InTable - self.depth_in_table += 1 - - def add_tag_to_html(self, tag, attrs=None): - self.final_html += f"<{tag}" - if attrs: - self.final_html += " " - for attr, value in attrs: - self.final_html += f"{attr}='{value}'" - self.final_html += ">\n" + self.widget.setUrl(self.url) - def handle_endtag(self, tag): - if tag == "table": - self.handle_table_end() - elif tag == "span": - self.handle_span_end() - elif tag == "div": - self.handle_div_end() - else: - if self.state == WikiCleaner.State.InMacroContent: - self.add_tag_to_html(f"/{tag}") + self.widget.setText( + translate("AddonsInstaller", "Loading page for {} from {}...").format( + self.addon.display_name, self.url + ) + ) - def handle_span_end(self): - if self.state == WikiCleaner.State.InEditSpan: - self.depth_in_span -= 1 - if self.depth_in_span <= 0: - self.depth_in_span = 0 - self.state = self.previous_state - else: - self.add_tag_to_html(f"/span") + if self.url[0] == "/": + if self.url.lower().endswith(".md"): + self.readme_data_type = ReadmeDataType.Markdown + elif self.url.lower().endswith(".html"): + self.readme_data_type = ReadmeDataType.Html - def handle_div_end(self): - if self.state == WikiCleaner.State.InMacroContent: - self.depth_in_div -= 1 - if self.depth_in_div <= 0: - self.depth_in_div = 0 - self.state = WikiCleaner.State.AfterMacroContent - self.final_html += "" + with open(self.url, "r") as fd: + self._process_package_download("".join(fd.readlines())) else: - self.add_tag_to_html(f"/div") - - def handle_table_end(self): - if self.state == WikiCleaner.State.InTable: - self.depth_in_table -= 1 - if self.depth_in_table <= 0: - self.depth_in_table = 0 - self.state = self.previous_state - - def handle_data(self, data): - if self.state == WikiCleaner.State.InMacroContent: - self.final_html += data + self.readme_request_index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get( + self.url + ) diff --git a/composite_view.py b/composite_view.py index 4d80c522..7e7a96ae 100644 --- a/composite_view.py +++ b/composite_view.py @@ -134,7 +134,7 @@ def _setup_connections(self): def addon_selected(self, addon): """Depending on the display_style, show addon details (possibly hiding the package_list widget in the process.""" - self.package_details_controller.show_repo(addon) + self.package_details_controller.show_addon(addon) if self.display_style != AddonManagerDisplayStyle.COMPOSITE: self.scroll_position = ( self.package_list.ui.listPackages.verticalScrollBar().sliderPosition()