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