Addon Manager: Break up ReadmeViewer into view and controller

Addon Manager: Cleanup enable/disable message
This commit is contained in:
Chris Hennes
2024-02-07 22:08:42 -06:00
committed by Chris Hennes
parent 20a01cfc9c
commit 9812548b68
15 changed files with 811 additions and 679 deletions

View File

@@ -52,7 +52,8 @@ from addonmanager_update_all_gui import UpdateAllGUI
import addonmanager_utilities as utils
import AddonManager_rc # This is required by Qt, it's not unused
from package_list import PackageList, PackageListItemModel
from package_details import PackageDetails
from addonmanager_package_details_controller import PackageDetailsController
from Widgets.addonmanager_widget_package_details_view import PackageDetailsView
from Widgets.addonmanager_widget_global_buttons import WidgetGlobalButtonBar
from Addon import Addon
from manage_python_dependencies import (
@@ -204,7 +205,8 @@ class CommandAddonManager:
self.dialog.layout().addWidget(self.button_bar)
# Package details start out hidden
self.packageDetails = PackageDetails(self.dialog)
self.packageDetails = PackageDetailsView(self.dialog)
self.package_details_controller = PackageDetailsController(self.packageDetails)
self.packageDetails.hide()
index = self.dialog.layout().indexOf(self.packageList)
self.dialog.layout().insertWidget(index, self.packageDetails)
@@ -238,12 +240,12 @@ class CommandAddonManager:
self.packageList.ui.progressBar.stop_clicked.connect(self.stop_update)
self.packageList.itemSelected.connect(self.table_row_activated)
self.packageList.setEnabled(False)
self.packageDetails.execute.connect(self.executemacro)
self.packageDetails.install.connect(self.launch_installer_gui)
self.packageDetails.uninstall.connect(self.remove)
self.packageDetails.update.connect(self.update)
self.packageDetails.back.connect(self.on_buttonBack_clicked)
self.packageDetails.update_status.connect(self.status_updated)
self.package_details_controller.execute.connect(self.executemacro)
self.package_details_controller.install.connect(self.launch_installer_gui)
self.package_details_controller.uninstall.connect(self.remove)
self.package_details_controller.update.connect(self.update)
self.package_details_controller.back.connect(self.on_buttonBack_clicked)
self.package_details_controller.update_status.connect(self.status_updated)
# center the dialog over the FreeCAD window
mw = FreeCADGui.getMainWindow()
@@ -735,7 +737,7 @@ class CommandAddonManager:
self.packageList.hide()
self.packageDetails.show()
self.packageDetails.show_repo(selected_repo)
self.package_details_controller.show_repo(selected_repo)
def show_information(self, message: str) -> None:
"""shows generic text in the information pane"""
@@ -746,7 +748,7 @@ class CommandAddonManager:
def show_workbench(self, repo: Addon) -> None:
self.packageList.hide()
self.packageDetails.show()
self.packageDetails.show_repo(repo)
self.package_details_controller.show_repo(repo)
def on_buttonBack_clicked(self) -> None:
self.packageDetails.hide()
@@ -765,7 +767,7 @@ class CommandAddonManager:
else:
repo.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
self.item_model.reload_item(repo)
self.packageDetails.show_repo(repo)
self.package_details_controller.show_repo(repo)
def launch_installer_gui(self, addon: Addon) -> None:
if self.installer_gui is not None:
@@ -860,7 +862,7 @@ class CommandAddonManager:
if repo.status() == Addon.Status.PENDING_RESTART:
self.restart_required = True
self.item_model.reload_item(repo)
self.packageDetails.show_repo(repo)
self.package_details_controller.show_repo(repo)
if repo in self.packages_with_updates:
self.packages_with_updates.remove(repo)
self.enable_updates(len(self.packages_with_updates))

View File

@@ -30,8 +30,9 @@ SET(AddonManager_SRCS
addonmanager_macro.py
addonmanager_macro_parser.py
addonmanager_metadata.py
addonmanager_package_details_controller.py
addonmanager_pyside_interface.py
addonmanager_readme_viewer.py
addonmanager_readme_controller.py
addonmanager_update_all_gui.py
addonmanager_uninstaller.py
addonmanager_uninstaller_gui.py
@@ -68,7 +69,7 @@ SET(AddonManager_SRCS
loading.html
manage_python_dependencies.py
NetworkManager.py
package_details.py
addonmanager_package_details_controller.py
package_list.py
PythonDependencyUpdateDialog.ui
select_toolbar_dialog.ui

View File

@@ -472,19 +472,21 @@ if HAVE_QTNETWORK:
sender.abort()
self.__launch_request(current_index, self.__create_get_request(url))
def __on_ssl_error(self, reply: str, errors: List[str]):
def __on_ssl_error(self, reply: str, errors: List[str] = None):
"""Called when an SSL error occurs: prints the error information."""
if HAVE_FREECAD:
FreeCAD.Console.PrintWarning(
translate("AddonsInstaller", "Error with encrypted connection") + "\n:"
)
FreeCAD.Console.PrintWarning(reply)
for error in errors:
FreeCAD.Console.PrintWarning(error)
if errors is not None:
for error in errors:
FreeCAD.Console.PrintWarning(error)
else:
print("Error with encrypted connection")
for error in errors:
print(error)
if errors is not None:
for error in errors:
print(error)
def __download_progress(self, bytesReceived: int, bytesTotal: int) -> None:
"""Monitors download progress and emits a progress_made signal"""

View File

@@ -1,9 +1,12 @@
SET(AddonManagerWidget_SRCS
__init__.py
addonmanager_colors.py
addonmanager_widget_addon_buttons.py
addonmanager_widget_filter_selector.py
addonmanager_widget_global_buttons.py
addonmanager_widget_package_details_view.py
addonmanager_widget_progress_bar.py
addonmanager_widget_readme_browser.py
addonmanager_widget_search.py
addonmanager_widget_view_control_bar.py
addonmanager_widget_view_selector.py

View File

@@ -0,0 +1,48 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2022-2024 FreeCAD Project Association *
# * *
# * 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/>. *
# * *
# ***************************************************************************
from enum import Enum, auto
import FreeCADGui
from PySide import QtGui
def is_darkmode() -> bool:
"""Heuristics to determine if we are in a darkmode stylesheet"""
pl = FreeCADGui.getMainWindow().palette()
return pl.color(QtGui.QPalette.Window).lightness() < 128
def warning_color_string() -> str:
"""A shade of red, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
return "rgb(255,105,97)" if is_darkmode() else "rgb(215,0,21)"
def bright_color_string() -> str:
"""A shade of green, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
return "rgb(48,219,91)" if is_darkmode() else "rgb(36,138,61)"
def attention_color_string() -> str:
"""A shade of orange, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
return "rgb(255,179,64)" if is_darkmode() else "rgb(255,149,0)"

View File

@@ -77,39 +77,37 @@ class WidgetAddonButtons(QtWidgets.QWidget):
def _setup_ui(self):
self.horizontal_layout = QtWidgets.QHBoxLayout()
self.horizontal_layout.setContentsMargins(0, 0, 0, 0)
self.back_button = QtWidgets.QToolButton(self)
self.install_button = QtWidgets.QPushButton(self)
self.uninstall_button = QtWidgets.QPushButton(self)
self.enable_button = QtWidgets.QPushButton(self)
self.disable_button = QtWidgets.QPushButton(self)
self.update_button = QtWidgets.QPushButton(self)
self.run_macro_button = QtWidgets.QPushButton(self)
self.change_branch_button = QtWidgets.QPushButton(self)
self.check_for_update_button = QtWidgets.QPushButton(self)
self.horizontal_layout.addWidget(self.back_button)
self.back = QtWidgets.QToolButton(self)
self.install = QtWidgets.QPushButton(self)
self.uninstall = QtWidgets.QPushButton(self)
self.enable = QtWidgets.QPushButton(self)
self.disable = QtWidgets.QPushButton(self)
self.update = QtWidgets.QPushButton(self)
self.run_macro = QtWidgets.QPushButton(self)
self.change_branch = QtWidgets.QPushButton(self)
self.check_for_update = QtWidgets.QPushButton(self)
self.horizontal_layout.addWidget(self.back)
self.horizontal_layout.addStretch()
self.horizontal_layout.addWidget(self.check_for_update_button)
self.horizontal_layout.addWidget(self.install_button)
self.horizontal_layout.addWidget(self.uninstall_button)
self.horizontal_layout.addWidget(self.enable_button)
self.horizontal_layout.addWidget(self.disable_button)
self.horizontal_layout.addWidget(self.update_button)
self.horizontal_layout.addWidget(self.run_macro_button)
self.horizontal_layout.addWidget(self.change_branch_button)
self.horizontal_layout.addWidget(self.check_for_update)
self.horizontal_layout.addWidget(self.install)
self.horizontal_layout.addWidget(self.uninstall)
self.horizontal_layout.addWidget(self.enable)
self.horizontal_layout.addWidget(self.disable)
self.horizontal_layout.addWidget(self.update)
self.horizontal_layout.addWidget(self.run_macro)
self.horizontal_layout.addWidget(self.change_branch)
self.setLayout(self.horizontal_layout)
def _set_icons(self):
self.back_button.setIcon(
QtGui.QIcon.fromTheme("back", QtGui.QIcon(":/icons/button_left.svg"))
)
self.back.setIcon(QtGui.QIcon.fromTheme("back", QtGui.QIcon(":/icons/button_left.svg")))
def retranslateUi(self, _):
self.check_for_update_button.setText(translate("AddonsInstaller", "Check for update"))
self.install_button.setText(translate("AddonsInstaller", "Install"))
self.uninstall_button.setText(translate("AddonsInstaller", "Uninstall"))
self.disable_button.setText(translate("AddonsInstaller", "Disable"))
self.enable_button.setText(translate("AddonsInstaller", "Enable"))
self.update_button.setText(translate("AddonsInstaller", "Update"))
self.run_macro_button.setText(translate("AddonsInstaller", "Run"))
self.change_branch_button.setText(translate("AddonsInstaller", "Change branch..."))
self.back_button.setToolTip(translate("AddonsInstaller", "Return to package list"))
self.check_for_update.setText(translate("AddonsInstaller", "Check for update"))
self.install.setText(translate("AddonsInstaller", "Install"))
self.uninstall.setText(translate("AddonsInstaller", "Uninstall"))
self.disable.setText(translate("AddonsInstaller", "Disable"))
self.enable.setText(translate("AddonsInstaller", "Enable"))
self.update.setText(translate("AddonsInstaller", "Update"))
self.run_macro.setText(translate("AddonsInstaller", "Run"))
self.change_branch.setText(translate("AddonsInstaller", "Change branch..."))
self.back.setToolTip(translate("AddonsInstaller", "Return to package list"))

View File

@@ -109,5 +109,5 @@ class WidgetGlobalButtonBar(QtWidgets.QWidget):
else:
self.update_all_addons.setEnabled(True)
self.update_all_addons.setText(
translate("AddonsInstaller", "Apply %1 available updates").format(updates)
translate("AddonsInstaller", "Apply {} available updates").format(updates)
)

View File

@@ -1,7 +1,7 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2022-2024 FreeCAD Project Association *
# * Copyright (c) 2022-2024 The FreeCAD Project Association AISBL *
# * *
# * This file is part of FreeCAD. *
# * *
@@ -21,6 +21,10 @@
# * *
# ***************************************************************************
from dataclasses import dataclass
from enum import Enum, auto
import os
from typing import Optional
try:
import FreeCAD
@@ -49,28 +53,286 @@ except ImportError:
from PySide import QtCore, QtWidgets
from .addonmanager_widget_addon_buttons import WidgetAddonButtons
from .addonmanager_widget_readme_browser import WidgetReadmeBrowser
from .addonmanager_colors import warning_color_string, attention_color_string, bright_color_string
class MessageType(Enum):
Message = auto()
Warning = auto()
Error = auto()
@dataclass
class UpdateInformation:
check_in_progress: bool = False
update_available: bool = False
detached_head: bool = False
version: str = ""
tag: str = ""
branch: Optional[str] = None
@dataclass
class WarningFlags:
obsolete: bool = False
python2: bool = False
required_freecad_version: Optional[str] = None
non_osi_approved = False
non_fsf_libre = False
class PackageDetailsView(QtWidgets.QWidget):
"""The view class for the package details"""
install_clicked = QtCore.Signal()
uninstall_clicked = QtCore.Signal()
enable_clicked = QtCore.Signal()
disable_clicked = QtCore.Signal()
update_clicked = QtCore.Signal()
check_for_updates = QtCore.Signal()
run_clicked = QtCore.Signal()
def __init__(self, parent: QtWidgets.QWidget = None):
super().__init__(parent)
self.button_bar = None
self.text_browser = None
self.readme_browser = None
self.message_label = None
self.location_label = None
self.installed = False
self.disabled = False
self.update_info = UpdateInformation()
self.warning_flags = WarningFlags()
self.installed_version = None
self.installed_branch = None
self.installed_timestamp = None
self.can_disable = True
self._setup_ui()
def _setup_ui(self):
self.vertical_layout = QtWidgets.QVBoxLayout(self)
self.button_bar = WidgetAddonButtons(self)
self.text_browser = QtWidgets.QTextBrowser(self)
self.readme_browser = WidgetReadmeBrowser(self)
self.message_label = QtWidgets.QLabel(self)
self.location_label = QtWidgets.QLabel(self)
self.location_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
self.vertical_layout.addWidget(self.button_bar)
self.vertical_layout.addWidget(self.text_browser)
self.vertical_layout.addWidget(self.message_label)
self.vertical_layout.addWidget(self.location_label)
self.vertical_layout.addWidget(self.readme_browser)
def set_location(self, location: Optional[str]):
if location is not None:
text = (
translate("AddonsInstaller", "Installation location")
+ ": "
+ os.path.normpath(location)
)
self.location_label.setText(text)
self.location_label.show()
else:
self.location_label.hide()
def set_installed(
self,
installed: bool,
on_date: Optional[str] = None,
version: Optional[str] = None,
branch: Optional[str] = None,
):
self.installed = installed
self.installed_timestamp = on_date
self.installed_version = version
self.installed_branch = branch
if not self.installed:
self.set_location(None)
self._sync_ui_state()
def set_update_available(self, info: UpdateInformation):
self.update_info = info
self._sync_ui_state()
def set_disabled(self, disabled: bool):
self.disabled = disabled
self._sync_ui_state()
def allow_disabling(self, allow: bool):
self.can_disable = allow
self._sync_ui_state()
def allow_running(self, show: bool):
self.button_bar.run_macro.setVisible(show)
def set_warning_flags(self, flags: WarningFlags):
self.warning_flags = flags
self._sync_ui_state()
def set_new_disabled_status(self, disabled: bool):
"""If the user just changed the enabled/disabled state of the addon, display a message
indicating that will not take place until restart. Do not call except in a case of a
state change during this run."""
if disabled:
message = translate(
"AddonsInstaller", "This Addon will be disabled next time you restart FreeCAD."
)
else:
message = translate(
"AddonsInstaller", "This Addon will be enabled next time you restart FreeCAD."
)
self.message_label.setText(f"<h3>{message}</h3>")
self.message_label.setStyleSheet("color:" + attention_color_string())
def set_new_branch(self, branch: str):
"""If the user just changed branches, update the message to show that a restart is
needed."""
message_string = "<h3>"
message_string += translate(
"AddonsInstaller", "Changed to branch '{}' -- please restart to use Addon."
).format(branch)
message_string += "</h3>"
self.message_label.setText(message_string)
self.message_label.setStyleSheet("color:" + attention_color_string())
def set_updated(self):
"""If the user has just updated the addon but not yet restarted, show an indication that
we are awaiting a restart."""
message = translate(
"AddonsInstaller", "This Addon has been updated. Restart FreeCAD to see changes."
)
self.message_label.setText(f"<h3>{message}</h3>")
self.message_label.setStyleSheet("color:" + attention_color_string())
def _sync_ui_state(self):
self._sync_button_state()
self._create_status_label_text()
def _sync_button_state(self):
self.button_bar.install.setVisible(not self.installed)
self.button_bar.uninstall.setVisible(self.installed)
if not self.installed:
self.button_bar.disable.hide()
self.button_bar.enable.hide()
self.button_bar.update.hide()
self.button_bar.check_for_update.hide()
else:
self.button_bar.update.setVisible(self.update_info.update_available)
if self.update_info.detached_head:
self.button_bar.check_for_update.hide()
else:
self.button_bar.check_for_update.setVisible(not self.update_info.update_available)
if self.can_disable:
self.button_bar.enable.setVisible(self.disabled)
self.button_bar.disable.setVisible(not self.disabled)
else:
self.button_bar.enable.hide()
self.button_bar.disable.hide()
def _create_status_label_text(self):
if self.installed:
installation_details = self._get_installation_details_string()
update_details = self._get_update_status_string()
message_text = f"{installation_details} {update_details}"
if self.disabled:
message_text += " [" + translate("AddonsInstaller", "Disabled") + "]"
self.message_label.setText(f"<h3>{message_text}</h3>")
if self.disabled:
self.message_label.setStyleSheet("color:" + warning_color_string())
elif self.update_info.update_available:
self.message_label.setStyleSheet("color:" + attention_color_string())
else:
self.message_label.setStyleSheet("color:" + bright_color_string())
self.message_label.show()
elif self._there_are_warnings_to_show():
warnings = self._get_warning_string()
self.message_label.setText(f"<h3>{warnings}</h3>")
self.message_label.setStyleSheet("color:" + warning_color_string())
self.message_label.show()
else:
self.message_label.hide()
def _get_installation_details_string(self) -> str:
version = self.installed_version
date = ""
installed_version_string = ""
if self.installed_timestamp:
date = QtCore.QLocale().toString(
QtCore.QDateTime.fromSecsSinceEpoch(int(round(self.installed_timestamp, 0))),
QtCore.QLocale.ShortFormat,
)
if version and date:
installed_version_string += (
translate("AddonsInstaller", "Version {version} installed on {date}").format(
version=version, date=date
)
+ ". "
)
elif version:
installed_version_string += (
translate("AddonsInstaller", "Version {version} installed") + "."
).format(version=version)
elif date:
installed_version_string += (
translate("AddonsInstaller", "Installed on {date}") + "."
).format(date=date)
else:
installed_version_string += translate("AddonsInstaller", "Installed") + "."
return installed_version_string
def _get_update_status_string(self) -> str:
if self.update_info.check_in_progress:
return translate("AddonsInstaller", "Update check in progress") + "."
if self.update_info.detached_head:
return (
translate(
"AddonsInstaller", "Git tag '{}' checked out, no updates possible"
).format(self.update_info.tag)
+ "."
)
if self.update_info.update_available:
if self.installed_branch and self.update_info.branch:
if self.installed_branch != self.update_info.branch:
return (
translate(
"AddonsInstaller", "Currently on branch {}, name changed to {}"
).format(self.installed_branch, self.update_info.branch)
+ "."
)
if self.update_info.version:
return (
translate(
"AddonsInstaller",
"Currently on branch {}, update available to version {}",
).format(self.installed_branch, str(self.update_info.version).strip())
+ "."
)
return translate("AddonsInstaller", "Update available") + "."
if self.update_info.version:
return (
translate("AddonsInstaller", "Update available to version {}").format(
str(self.update_info.version).strip()
)
+ "."
)
return translate("AddonsInstaller", "Update available") + "."
return translate("AddonsInstaller", "This is the latest version available") + "."
def _there_are_warnings_to_show(self) -> bool:
if self.disabled:
return True
if (
self.warning_flags.obsolete
or self.warning_flags.python2
or self.warning_flags.required_freecad_version
):
return True
return False # TODO: Someday support optional warnings on license types
def _get_warning_string(self) -> str:
if self.installed and self.disabled:
return translate(
"AddonsInstaller",
"WARNING: This addon is currently installed, but disabled. Use the 'enable' "
"button to re-enable.",
)
if self.warning_flags.obsolete:
return translate("AddonsInstaller", "WARNING: This addon is obsolete")
if self.warning_flags.python2:
return translate("AddonsInstaller", "WARNING: This addon is Python 2 only")
if self.warning_flags.required_freecad_version:
return translate("AddonsInstaller", "WARNING: This addon requires FreeCAD {}").format(
self.warning_flags.required_freecad_version
)
return ""

View File

@@ -0,0 +1,111 @@
# 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 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
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."""
if hasattr(super(), "setMarkdown"):
super().setMarkdown(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"
)
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]
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

View File

@@ -119,6 +119,7 @@ class WidgetViewSelector(QtWidgets.QWidget):
self.composite_button.setIcon(
QtGui.QIcon.fromTheme("composite_button", QtGui.QIcon(":/icons/composite_view.svg"))
)
self.composite_button.hide() # TODO: Implement this view
self.horizontal_layout.addWidget(self.compact_button)
self.horizontal_layout.addWidget(self.expanded_button)

View File

View File

@@ -0,0 +1,258 @@
# 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/>. *
# * *
# ***************************************************************************
""" Provides the PackageDetails widget. """
import os
from typing import Optional
from PySide import QtCore, QtWidgets
import addonmanager_freecad_interface as fci
import addonmanager_utilities as utils
from addonmanager_metadata import (
Version,
get_first_supported_freecad_version,
get_branch_from_metadata,
)
from addonmanager_workers_startup import GetMacroDetailsWorker, CheckSingleUpdateWorker
from addonmanager_git import GitManager, NoGitFound
from Addon import Addon
from change_branch import ChangeBranchDialog
from addonmanager_readme_controller import ReadmeController
from Widgets.addonmanager_widget_package_details_view import UpdateInformation, WarningFlags
translate = fci.translate
class PackageDetailsController(QtCore.QObject):
"""Manages the display of the package README information."""
back = QtCore.Signal()
install = QtCore.Signal(Addon)
uninstall = QtCore.Signal(Addon)
update = QtCore.Signal(Addon)
execute = QtCore.Signal(Addon)
update_status = QtCore.Signal(Addon)
check_for_update = QtCore.Signal(Addon)
def __init__(self, widget=None):
super().__init__()
self.ui = widget
self.readme_controller = ReadmeController(self.ui.readme_browser)
self.worker = None
self.addon = None
self.status_update_thread = None
self.original_disabled_state = None
self.original_status = None
try:
self.git_manager = GitManager()
except NoGitFound:
self.git_manager = None
self.ui.button_bar.back.clicked.connect(self.back.emit)
self.ui.button_bar.run_macro.clicked.connect(lambda: self.execute.emit(self.addon))
self.ui.button_bar.install.clicked.connect(lambda: self.install.emit(self.addon))
self.ui.button_bar.uninstall.clicked.connect(lambda: self.uninstall.emit(self.addon))
self.ui.button_bar.update.clicked.connect(lambda: self.update.emit(self.addon))
self.ui.button_bar.check_for_update.clicked.connect(
lambda: self.check_for_update.emit(self.addon)
)
self.ui.button_bar.change_branch.clicked.connect(self.change_branch_clicked)
self.ui.button_bar.enable.clicked.connect(self.enable_clicked)
self.ui.button_bar.disable.clicked.connect(self.disable_clicked)
def show_repo(self, repo: Addon) -> None:
"""The main entry point for this class, shows the package details and related buttons
for the provided repo."""
self.addon = repo
self.readme_controller.set_addon(repo)
self.original_disabled_state = self.addon.is_disabled()
if self.worker is not None:
if not self.worker.isFinished():
self.worker.requestInterruption()
self.worker.wait()
installed = self.addon.status() != Addon.Status.NOT_INSTALLED
self.ui.set_installed(installed)
update_info = UpdateInformation()
if installed:
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 repo.metadata:
update_info.branch = get_branch_from_metadata(repo.metadata)
update_info.version = repo.metadata.version
elif repo.macro:
update_info.version = repo.macro.version
self.ui.set_update_available(update_info)
self.ui.set_location(os.path.join(self.addon.mod_directory, self.addon.name))
self.ui.set_location(os.path.join(self.addon.mod_directory, self.addon.name))
self.ui.set_disabled(self.addon.is_disabled())
self.ui.allow_running(repo.repo_type == Addon.Kind.MACRO)
self.ui.allow_disabling(repo.repo_type != Addon.Kind.MACRO)
if repo.repo_type == Addon.Kind.MACRO:
self.update_macro_info(repo)
if repo.status() == Addon.Status.UNCHECKED:
if not self.status_update_thread:
self.status_update_thread = QtCore.QThread()
self.status_create_addon_list_worker = CheckSingleUpdateWorker(repo)
self.status_create_addon_list_worker.moveToThread(self.status_update_thread)
self.status_update_thread.finished.connect(
self.status_create_addon_list_worker.deleteLater
)
self.check_for_update.connect(self.status_create_addon_list_worker.do_work)
self.status_create_addon_list_worker.update_status.connect(self.display_repo_status)
self.status_update_thread.start()
update_info.check_in_progress = True
self.ui.set_update_available(update_info)
self.check_for_update.emit(self.addon)
flags = WarningFlags()
flags.required_freecad_version = self.requires_newer_freecad()
flags.obsolete = repo.obsolete
flags.python2 = repo.python2
self.ui.set_warning_flags(flags)
def requires_newer_freecad(self) -> Optional[Version]:
"""If the current package is not installed, returns the first supported version of
FreeCAD, if one is set, or None if no information is available (or if the package is
already installed)."""
# If it's not installed, check to see if it's for a newer version of FreeCAD
if self.addon.status() == Addon.Status.NOT_INSTALLED and self.addon.metadata:
# Only hide if ALL content items require a newer version, otherwise
# it's possible that this package actually provides versions of itself
# for newer and older versions
first_supported_version = get_first_supported_freecad_version(self.addon.metadata)
if first_supported_version is not None:
fc_version = Version(from_list=fci.Version())
if first_supported_version > fc_version:
return first_supported_version
return None
def set_change_branch_button_state(self):
"""The change branch button is only available for installed Addons that have a .git directory
and in runs where the git is available."""
self.ui.button_bar.change_branch_button.hide()
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons")
show_switcher = pref.GetBool("ShowBranchSwitcher", False)
if not show_switcher:
return
# Is this repo installed? If not, return.
if self.addon.status() == Addon.Status.NOT_INSTALLED:
return
# Is it a Macro? If so, return:
if self.addon.repo_type == Addon.Kind.MACRO:
return
# Can we actually switch branches? If not, return.
if not self.git_manager:
return
# Is there a .git subdirectory? If not, return.
basedir = fci.getUserAppDataDir()
path_to_git = os.path.join(basedir, "Mod", self.addon.name, ".git")
if not os.path.isdir(path_to_git):
return
# If all four above checks passed, then it's possible for us to switch
# branches, if there are any besides the one we are on: show the button
self.ui.button_bar.change_branch_button.show()
def update_macro_info(self, repo: Addon) -> None:
if not repo.macro.url:
# 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.readme_updated.connect(self.macro_readme_updated)
self.worker.start()
def change_branch_clicked(self) -> None:
"""Loads the branch-switching dialog"""
basedir = fci.getUserAppDataDir()
path_to_repo = os.path.join(basedir, "Mod", self.addon.name)
change_branch_dialog = ChangeBranchDialog(path_to_repo, self.ui)
change_branch_dialog.branch_changed.connect(self.branch_changed)
change_branch_dialog.exec()
def enable_clicked(self) -> None:
"""Called by the Enable button, enables this Addon and updates GUI to reflect
that status."""
self.addon.enable()
self.ui.set_disabled(False)
if self.original_disabled_state:
self.ui.set_new_disabled_status(False)
self.original_status = self.addon.status()
self.addon.set_status(Addon.Status.PENDING_RESTART)
else:
self.addon.set_status(self.original_status)
self.update_status.emit(self.addon)
def disable_clicked(self) -> None:
"""Called by the Disable button, disables this Addon and updates the GUI to
reflect that status."""
self.addon.disable()
self.ui.set_disabled(True)
if not self.original_disabled_state:
self.ui.set_new_disabled_status(True)
self.original_status = self.addon.status()
self.addon.set_status(Addon.Status.PENDING_RESTART)
else:
self.addon.set_status(self.original_status)
self.update_status.emit(self.addon)
def branch_changed(self, name: str) -> None:
"""Displays a dialog confirming the branch changed, and tries to access the
metadata file from that branch."""
QtWidgets.QMessageBox.information(
self.ui,
translate("AddonsInstaller", "Success"),
translate(
"AddonsInstaller",
"Branch change succeeded, please restart to use the new version.",
),
)
# See if this branch has a package.xml file:
basedir = fci.getUserAppDataDir()
path_to_metadata = os.path.join(basedir, "Mod", self.addon.name, "package.xml")
if os.path.isfile(path_to_metadata):
self.addon.load_metadata_file(path_to_metadata)
self.addon.installed_version = self.addon.metadata.version
else:
self.addon.repo_type = Addon.Kind.WORKBENCH
self.addon.metadata = None
self.addon.installed_version = None
self.addon.updated_timestamp = QtCore.QDateTime.currentDateTime().toSecsSinceEpoch()
self.addon.branch = name
self.addon.set_status(Addon.Status.PENDING_RESTART)
self.ui.set_new_branch(name)
self.update_status.emit(self.addon)

View File

@@ -23,50 +23,66 @@
""" A Qt Widget for displaying Addon README information """
import Addon
from PySide import QtCore, QtGui, QtWidgets
from enum import Enum, auto
from html.parser import HTMLParser
import addonmanager_freecad_interface as fci
import FreeCAD
from Addon import Addon
import addonmanager_utilities as utils
from enum import IntEnum, Enum, auto
from html.parser import HTMLParser
from typing import Optional
import NetworkManager
translate = fci.translate
translate = FreeCAD.Qt.translate
from PySide import QtCore, QtGui
class ReadmeViewer(QtWidgets.QTextBrowser):
class ReadmeDataType(IntEnum):
PlainText = 0
Markdown = 1
Html = 2
"""A QTextBrowser widget that, when given an Addon, downloads the README data as appropriate
and renders it with whatever technology is available (usually Qt's Markdown renderer for
workbenches and its HTML renderer for Macros)."""
def __init__(self, parent=None):
super().__init__(parent)
class ReadmeController(QtCore.QObject):
"""A class that can provide README data from an Addon, possibly loading external resources such
as images"""
def __init__(self, widget):
super().__init__()
NetworkManager.InitializeNetworkManager()
NetworkManager.AM_NETWORK_MANAGER.completed.connect(self._download_completed)
self.readme_request_index = 0
self.resource_requests = {}
self.resource_failures = []
self.url = ""
self.repo: Addon.Addon = None
self.setOpenExternalLinks(True)
self.setOpenLinks(True)
self.image_map = {}
self.readme_data = None
self.readme_data_type = None
self.addon: Optional[Addon] = None
self.stop = True
self.widget = widget
self.widget.load_resource.connect(self.loadResource)
def set_addon(self, repo: Addon):
"""Set which Addon's information is displayed"""
self.setPlainText(translate("AddonsInstaller", "Loading README data..."))
self.repo = repo
self.addon = repo
self.stop = False
if self.repo.repo_type == Addon.Addon.Kind.MACRO:
self.url = self.repo.macro.wiki
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.repo.macro.url
self.url = self.addon.macro.url
else:
self.url = utils.get_readme_url(repo)
self.widget.setUrl(self.url)
self.widget.setText(
translate("AddonsInstaller", "Loading page for {} from {}...").format(
self.addon.display_name, self.url
)
)
self.readme_request_index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
self.url
)
@@ -77,7 +93,7 @@ class ReadmeViewer(QtWidgets.QTextBrowser):
if code == 200: # HTTP success
self._process_package_download(data.data().decode("utf-8"))
else:
self.setPlainText(
self.widget.setText(
translate(
"AddonsInstaller",
"Failed to download data from {} -- received response code {}.",
@@ -87,47 +103,42 @@ class ReadmeViewer(QtWidgets.QTextBrowser):
if code == 200:
self._process_resource_download(self.resource_requests[index], data.data())
else:
self.image_map[self.resource_requests[index]] = None
FreeCAD.Console.PrintLog(f"Failed to load {self.resource_requests[index]}\n")
self.resource_failures.append(self.resource_requests[index])
del self.resource_requests[index]
if not self.resource_requests:
self.set_addon(self.repo) # Trigger a reload of the page now with resources
if self.readme_data:
if self.readme_data_type == ReadmeDataType.Html:
self.widget.setHtml(self.readme_data)
elif self.readme_data_type == ReadmeDataType.Markdown:
self.widget.setMarkdown(self.readme_data)
else:
self.widget.setText(self.readme_data)
else:
self.set_addon(self.addon) # Trigger a reload of the page now with resources
def _process_package_download(self, data: str):
if self.repo.repo_type == Addon.Addon.Kind.MACRO:
if self.addon.repo_type == Addon.Kind.MACRO:
parser = WikiCleaner()
parser.feed(data)
self.setHtml(parser.final_html)
self.readme_data = parser.final_html
self.readme_data_type = ReadmeDataType.Html
self.widget.setHtml(parser.final_html)
else:
# Check for recent Qt (e.g. Qt5.15 or later). Check can be removed when
# we no longer support Ubuntu 20.04LTS for compiling.
if hasattr(self, "setMarkdown"):
self.setMarkdown(data)
else:
self.setPlainText(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)
if image:
self.image_map[resource_name] = self._ensure_appropriate_width(image)
else:
self.image_map[resource_name] = None
self.widget.set_resource(resource_name, 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.image_map[full_url] = None
fci.Console.PrintMessage(f"Downloading image from {full_url}...\n")
index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(full_url)
self.resource_requests[index] = full_url
return self.image_map[full_url]
return super().loadResource(resource_type, name)
def loadResource(self, full_url: str):
if full_url not in self.resource_failures:
index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(full_url)
self.resource_requests[index] = full_url
def hideEvent(self, event: QtGui.QHideEvent):
def cancel_resource_loading(self):
self.stop = True
for request in self.resource_requests:
NetworkManager.AM_NETWORK_MANAGER.abort(request)
@@ -141,12 +152,6 @@ class ReadmeViewer(QtWidgets.QTextBrowser):
lhs, slash, _ = self.url.rpartition("/")
return lhs + slash + url
def _ensure_appropriate_width(self, image: QtGui.QImage) -> QtGui.QImage:
ninety_seven_percent = self.width() * 0.97
if image.width() < ninety_seven_percent:
return image
return image.scaledToWidth(ninety_seven_percent)
class WikiCleaner(HTMLParser):
"""This HTML parser cleans up FreeCAD Macro Wiki Page for display in a

View File

@@ -1,559 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2022-2023 FreeCAD Project Association *
# * *
# * 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/>. *
# * *
# ***************************************************************************
""" Provides the PackageDetails widget. """
import os
from typing import Optional
from PySide import QtCore, QtGui, QtWidgets
import addonmanager_freecad_interface as fci
import addonmanager_utilities as utils
from addonmanager_metadata import (
Version,
get_first_supported_freecad_version,
get_branch_from_metadata,
)
from addonmanager_workers_startup import GetMacroDetailsWorker, CheckSingleUpdateWorker
from addonmanager_readme_viewer import ReadmeViewer
from addonmanager_git import GitManager, NoGitFound
from Addon import Addon
from change_branch import ChangeBranchDialog
from Widgets.addonmanager_widget_addon_buttons import WidgetAddonButtons
translate = fci.translate
class PackageDetails(QtWidgets.QWidget):
"""The PackageDetails QWidget shows package README information and provides
install, uninstall, and update buttons."""
back = QtCore.Signal()
install = QtCore.Signal(Addon)
uninstall = QtCore.Signal(Addon)
update = QtCore.Signal(Addon)
execute = QtCore.Signal(Addon)
update_status = QtCore.Signal(Addon)
check_for_update = QtCore.Signal(Addon)
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_PackageDetails()
self.ui.setupUi(self)
self.worker = None
self.repo = None
self.status_update_thread = None
try:
self.git_manager = GitManager()
except NoGitFound:
self.git_manager = None
self.ui.button_bar.back_button.clicked.connect(self.back.emit)
self.ui.button_bar.run_macro_button.clicked.connect(lambda: self.execute.emit(self.repo))
self.ui.button_bar.install_button.clicked.connect(lambda: self.install.emit(self.repo))
self.ui.button_bar.uninstall_button.clicked.connect(lambda: self.uninstall.emit(self.repo))
self.ui.button_bar.update_button.clicked.connect(lambda: self.update.emit(self.repo))
self.ui.button_bar.check_for_update_button.clicked.connect(
lambda: self.check_for_update.emit(self.repo)
)
self.ui.button_bar.change_branch_button.clicked.connect(self.change_branch_clicked)
self.ui.button_bar.enable_button.clicked.connect(self.enable_clicked)
self.ui.button_bar.disable_button.clicked.connect(self.disable_clicked)
def show_repo(self, repo: Addon, reload: bool = False) -> None:
"""The main entry point for this class, shows the package details and related buttons
for the provided repo. If reload is true, then even if this is already the current repo
the data is reloaded."""
# If this is the same repo we were already showing, we do not have to do the
# expensive refetch unless reload is true
if True or self.repo != repo or reload:
self.repo = repo
if self.worker is not None:
if not self.worker.isFinished():
self.worker.requestInterruption()
self.worker.wait()
if repo.repo_type == Addon.Kind.MACRO:
self.show_macro(repo)
self.ui.button_bar.run_macro_button.show()
elif repo.repo_type == Addon.Kind.WORKBENCH:
self.show_workbench(repo)
self.ui.button_bar.run_macro_button.hide()
elif repo.repo_type == Addon.Kind.PACKAGE:
self.show_package(repo)
self.ui.button_bar.run_macro_button.hide()
if repo.status() == Addon.Status.UNCHECKED:
if not self.status_update_thread:
self.status_update_thread = QtCore.QThread()
self.status_create_addon_list_worker = CheckSingleUpdateWorker(repo)
self.status_create_addon_list_worker.moveToThread(self.status_update_thread)
self.status_update_thread.finished.connect(
self.status_create_addon_list_worker.deleteLater
)
self.check_for_update.connect(self.status_create_addon_list_worker.do_work)
self.status_create_addon_list_worker.update_status.connect(self.display_repo_status)
self.status_update_thread.start()
self.check_for_update.emit(self.repo)
self.display_repo_status(self.repo.update_status)
def display_repo_status(self, status):
"""Updates the contents of the widget to display the current install status of the widget."""
repo = self.repo
self.set_change_branch_button_state()
self.set_disable_button_state()
if status != Addon.Status.NOT_INSTALLED:
version = repo.installed_version
date = ""
installed_version_string = "<h3>"
if repo.updated_timestamp:
date = QtCore.QLocale().toString(
QtCore.QDateTime.fromSecsSinceEpoch(int(round(repo.updated_timestamp, 0))),
QtCore.QLocale.ShortFormat,
)
if version and date:
installed_version_string += (
translate("AddonsInstaller", "Version {version} installed on {date}").format(
version=version, date=date
)
+ ". "
)
elif version:
installed_version_string += (
translate("AddonsInstaller", "Version {version} installed") + ". "
).format(version=version)
elif date:
installed_version_string += (
translate("AddonsInstaller", "Installed on {date}") + ". "
).format(date=date)
else:
installed_version_string += translate("AddonsInstaller", "Installed") + ". "
if status == Addon.Status.UPDATE_AVAILABLE:
if repo.metadata:
name_change = False
if repo.installed_metadata:
old_branch = get_branch_from_metadata(repo.installed_metadata)
new_branch = get_branch_from_metadata(repo.metadata)
if old_branch != new_branch:
installed_version_string += (
"<b>"
+ translate(
"AddonsInstaller",
"Currently on branch {}, name changed to {}",
).format(old_branch, new_branch)
) + ".</b> "
name_change = True
if not name_change:
installed_version_string += (
"<b>"
+ translate(
"AddonsInstaller",
"On branch {}, update available to version",
).format(repo.branch)
+ " "
)
installed_version_string += str(repo.metadata.version)
installed_version_string += ".</b>"
elif repo.macro and repo.macro.version:
installed_version_string += (
"<b>" + translate("AddonsInstaller", "Update available to version") + " "
)
installed_version_string += repo.macro.version
installed_version_string += ".</b>"
else:
installed_version_string += (
"<b>"
+ translate(
"AddonsInstaller",
"An update is available",
)
+ ".</b>"
)
elif status == Addon.Status.NO_UPDATE_AVAILABLE:
detached_head = False
branch = repo.branch
if self.git_manager and repo.repo_type != Addon.Kind.MACRO:
basedir = fci.getUserAppDataDir()
moddir = os.path.join(basedir, "Mod", repo.name)
repo_path = os.path.join(moddir, ".git")
if os.path.exists(repo_path):
branch = self.git_manager.current_branch(repo_path)
if self.git_manager.detached_head(repo_path):
tag = self.git_manager.current_tag(repo_path)
branch = tag
detached_head = True
if detached_head:
installed_version_string += (
translate(
"AddonsInstaller",
"Git tag '{}' checked out, no updates possible",
).format(branch)
+ "."
)
else:
installed_version_string += (
translate(
"AddonsInstaller",
"This is the latest version available for branch {}",
).format(branch)
+ "."
)
elif status == Addon.Status.PENDING_RESTART:
installed_version_string += (
translate("AddonsInstaller", "Updated, please restart FreeCAD to use") + "."
)
elif status == Addon.Status.UNCHECKED:
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons")
autocheck = pref.GetBool("AutoCheck", False)
if autocheck:
installed_version_string += (
translate("AddonsInstaller", "Update check in progress") + "."
)
else:
installed_version_string += (
translate("AddonsInstaller", "Automatic update checks disabled") + "."
)
installed_version_string += "</h3>"
self.ui.labelPackageDetails.setText(installed_version_string)
if repo.status() == Addon.Status.UPDATE_AVAILABLE:
self.ui.labelPackageDetails.setStyleSheet("color:" + utils.attention_color_string())
else:
self.ui.labelPackageDetails.setStyleSheet("color:" + utils.bright_color_string())
self.ui.labelPackageDetails.show()
if repo.macro is not None:
moddir = fci.getUserMacroDir(True)
else:
basedir = fci.getUserAppDataDir()
moddir = os.path.join(basedir, "Mod", repo.name)
installationLocationString = (
translate("AddonsInstaller", "Installation location")
+ ": "
+ os.path.normpath(moddir)
)
self.ui.labelInstallationLocation.setText(installationLocationString)
self.ui.labelInstallationLocation.show()
else:
self.ui.labelPackageDetails.hide()
self.ui.labelInstallationLocation.hide()
if status == Addon.Status.NOT_INSTALLED:
self.ui.button_bar.install_button.show()
self.ui.button_bar.uninstall_button.hide()
self.ui.button_bar.update_button.hide()
self.ui.button_bar.check_for_update_button.hide()
elif status == Addon.Status.NO_UPDATE_AVAILABLE:
self.ui.button_bar.install_button.hide()
self.ui.button_bar.uninstall_button.show()
self.ui.button_bar.update_button.hide()
self.ui.button_bar.check_for_update_button.hide()
elif status == Addon.Status.UPDATE_AVAILABLE:
self.ui.button_bar.install_button.hide()
self.ui.button_bar.uninstall_button.show()
self.ui.button_bar.update_button.show()
self.ui.button_bar.check_for_update_button.hide()
elif status == Addon.Status.UNCHECKED:
self.ui.button_bar.install_button.hide()
self.ui.button_bar.uninstall_button.show()
self.ui.button_bar.update_button.hide()
self.ui.button_bar.check_for_update_button.show()
elif status == Addon.Status.PENDING_RESTART:
self.ui.button_bar.install_button.hide()
self.ui.button_bar.uninstall_button.show()
self.ui.button_bar.update_button.hide()
self.ui.button_bar.check_for_update_button.hide()
elif status == Addon.Status.CANNOT_CHECK:
self.ui.button_bar.install_button.hide()
self.ui.button_bar.uninstall_button.show()
self.ui.button_bar.update_button.show()
self.ui.button_bar.check_for_update_button.hide()
required_version = self.requires_newer_freecad()
if repo.obsolete:
self.ui.labelWarningInfo.show()
self.ui.labelWarningInfo.setText(
"<h1>" + translate("AddonsInstaller", "WARNING: This addon is obsolete") + "</h1>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + utils.warning_color_string())
elif repo.python2:
self.ui.labelWarningInfo.show()
self.ui.labelWarningInfo.setText(
"<h1>"
+ translate("AddonsInstaller", "WARNING: This addon is Python 2 Only")
+ "</h1>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + utils.warning_color_string())
elif required_version:
self.ui.labelWarningInfo.show()
self.ui.labelWarningInfo.setText(
"<h1>"
+ translate("AddonsInstaller", "WARNING: This addon requires FreeCAD ")
+ required_version
+ "</h1>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + utils.warning_color_string())
elif repo.is_disabled():
self.ui.labelWarningInfo.show()
self.ui.labelWarningInfo.setText(
"<h2>"
+ translate(
"AddonsInstaller",
"WARNING: This addon is currently installed, but disabled. Use the 'enable' button to re-enable.",
)
+ "</h2>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + utils.warning_color_string())
else:
self.ui.labelWarningInfo.hide()
def requires_newer_freecad(self) -> Optional[Version]:
"""If the current package is not installed, returns the first supported version of
FreeCAD, if one is set, or None if no information is available (or if the package is
already installed)."""
# If it's not installed, check to see if it's for a newer version of FreeCAD
if self.repo.status() == Addon.Status.NOT_INSTALLED and self.repo.metadata:
# Only hide if ALL content items require a newer version, otherwise
# it's possible that this package actually provides versions of itself
# for newer and older versions
first_supported_version = get_first_supported_freecad_version(self.repo.metadata)
if first_supported_version is not None:
fc_version = Version(from_list=fci.Version())
if first_supported_version > fc_version:
return first_supported_version
return None
def set_change_branch_button_state(self):
"""The change branch button is only available for installed Addons that have a .git directory
and in runs where the git is available."""
self.ui.button_bar.change_branch_button.hide()
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons")
show_switcher = pref.GetBool("ShowBranchSwitcher", False)
if not show_switcher:
return
# Is this repo installed? If not, return.
if self.repo.status() == Addon.Status.NOT_INSTALLED:
return
# Is it a Macro? If so, return:
if self.repo.repo_type == Addon.Kind.MACRO:
return
# Can we actually switch branches? If not, return.
if not self.git_manager:
return
# Is there a .git subdirectory? If not, return.
basedir = fci.getUserAppDataDir()
path_to_git = os.path.join(basedir, "Mod", self.repo.name, ".git")
if not os.path.isdir(path_to_git):
return
# If all four above checks passed, then it's possible for us to switch
# branches, if there are any besides the one we are on: show the button
self.ui.button_bar.change_branch_button.show()
def set_disable_button_state(self):
"""Set up the enable/disable button based on the enabled/disabled state of the addon"""
self.ui.button_bar.enable_button.hide()
self.ui.button_bar.disable_button.hide()
status = self.repo.status()
if status != Addon.Status.NOT_INSTALLED:
disabled = self.repo.is_disabled()
if disabled:
self.ui.button_bar.enable_button.show()
else:
self.ui.button_bar.disable_button.show()
def show_workbench(self, repo: Addon) -> None:
"""loads information of a given workbench"""
self.ui.textBrowserReadMe.set_addon(repo)
def show_package(self, repo: Addon) -> None:
"""Show the details for a package (a repo with a package.xml metadata file)"""
self.ui.textBrowserReadMe.set_addon(repo)
def show_macro(self, repo: Addon) -> None:
"""loads information of a given macro"""
if not repo.macro.url:
# 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.readme_updated.connect(self.macro_readme_updated)
self.worker.start()
else:
self.macro_readme_updated()
def macro_readme_updated(self):
"""Update the display of a Macro's README data."""
self.ui.textBrowserReadMe.set_addon(self.repo)
def change_branch_clicked(self) -> None:
"""Loads the branch-switching dialog"""
basedir = fci.getUserAppDataDir()
path_to_repo = os.path.join(basedir, "Mod", self.repo.name)
change_branch_dialog = ChangeBranchDialog(path_to_repo, self)
change_branch_dialog.branch_changed.connect(self.branch_changed)
change_branch_dialog.exec()
def enable_clicked(self) -> None:
"""Called by the Enable button, enables this Addon and updates GUI to reflect
that status."""
self.repo.enable()
self.repo.set_status(Addon.Status.PENDING_RESTART)
self.set_disable_button_state()
self.update_status.emit(self.repo)
self.ui.labelWarningInfo.show()
self.ui.labelWarningInfo.setText(
"<h3>"
+ translate(
"AddonsInstaller",
"This Addon will be enabled next time you restart FreeCAD.",
)
+ "</h3>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + utils.bright_color_string())
def disable_clicked(self) -> None:
"""Called by the Disable button, disables this Addon and updates the GUI to
reflect that status."""
self.repo.disable()
self.repo.set_status(Addon.Status.PENDING_RESTART)
self.set_disable_button_state()
self.update_status.emit(self.repo)
self.ui.labelWarningInfo.show()
self.ui.labelWarningInfo.setText(
"<h3>"
+ translate(
"AddonsInstaller",
"This Addon will be disabled next time you restart FreeCAD.",
)
+ "</h3>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + utils.attention_color_string())
def branch_changed(self, name: str) -> None:
"""Displays a dialog confirming the branch changed, and tries to access the
metadata file from that branch."""
QtWidgets.QMessageBox.information(
self,
translate("AddonsInstaller", "Success"),
translate(
"AddonsInstaller",
"Branch change succeeded, please restart to use the new version.",
),
)
# See if this branch has a package.xml file:
basedir = fci.getUserAppDataDir()
path_to_metadata = os.path.join(basedir, "Mod", self.repo.name, "package.xml")
if os.path.isfile(path_to_metadata):
self.repo.load_metadata_file(path_to_metadata)
self.repo.installed_version = self.repo.metadata.version
else:
self.repo.repo_type = Addon.Kind.WORKBENCH
self.repo.metadata = None
self.repo.installed_version = None
self.repo.updated_timestamp = QtCore.QDateTime.currentDateTime().toSecsSinceEpoch()
self.repo.branch = name
self.repo.set_status(Addon.Status.PENDING_RESTART)
installed_version_string = "<h3>"
installed_version_string += translate(
"AddonsInstaller", "Changed to git ref '{}' -- please restart to use Addon."
).format(name)
installed_version_string += "</h3>"
self.ui.labelPackageDetails.setText(installed_version_string)
self.ui.labelPackageDetails.setStyleSheet("color:" + utils.attention_color_string())
self.update_status.emit(self.repo)
class Ui_PackageDetails(object):
"""The generated UI from the Qt Designer UI file"""
def setupUi(self, PackageDetails):
if not PackageDetails.objectName():
PackageDetails.setObjectName("PackageDetails")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(PackageDetails)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.layoutDetailsBackButton = QtWidgets.QHBoxLayout()
self.layoutDetailsBackButton.setObjectName("layoutDetailsBackButton")
self.button_bar = WidgetAddonButtons(PackageDetails)
self.layoutDetailsBackButton.addWidget(self.button_bar)
self.verticalLayout_2.addLayout(self.layoutDetailsBackButton)
self.labelPackageDetails = QtWidgets.QLabel(PackageDetails)
self.labelPackageDetails.hide()
self.verticalLayout_2.addWidget(self.labelPackageDetails)
self.labelInstallationLocation = QtWidgets.QLabel(PackageDetails)
self.labelInstallationLocation.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
self.labelInstallationLocation.hide()
self.verticalLayout_2.addWidget(self.labelInstallationLocation)
self.labelWarningInfo = QtWidgets.QLabel(PackageDetails)
self.labelWarningInfo.hide()
self.verticalLayout_2.addWidget(self.labelWarningInfo)
sizePolicy1 = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
self.textBrowserReadMe = ReadmeViewer(PackageDetails)
self.textBrowserReadMe.setObjectName("textBrowserReadMe")
self.verticalLayout_2.addWidget(self.textBrowserReadMe)
self.retranslateUi(PackageDetails)
QtCore.QMetaObject.connectSlotsByName(PackageDetails)
# setupUi
def retranslateUi(self, _):
pass
# retranslateUi

0
src/__init__.py Normal file
View File