From 7a17106776de55b06dccd92b8f50aa9cc4c0e61d Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Thu, 1 Sep 2022 20:47:37 -0500 Subject: [PATCH] Addon Manager: Implement content addition dialogs --- src/Mod/AddonManager/AddonManager.py | 9 +- src/Mod/AddonManager/CMakeLists.txt | 4 + src/Mod/AddonManager/addonmanager_devmode.py | 41 +- .../addonmanager_devmode_add_content.py | 487 +++++++++++++ .../developer_mode_add_content.ui | 642 ++++++++++++------ ...eveloper_mode_advanced_freecad_versions.ui | 131 ++++ .../developer_mode_dependencies.ui | 238 ++----- .../developer_mode_edit_dependency.ui | 105 +++ .../developer_mode_freecad_versions.ui | 99 +++ src/Mod/AddonManager/developer_mode_tags.ui | 86 +++ 10 files changed, 1462 insertions(+), 380 deletions(-) create mode 100644 src/Mod/AddonManager/developer_mode_advanced_freecad_versions.ui create mode 100644 src/Mod/AddonManager/developer_mode_edit_dependency.ui create mode 100644 src/Mod/AddonManager/developer_mode_freecad_versions.ui create mode 100644 src/Mod/AddonManager/developer_mode_tags.ui diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index a6aacb2f7e..aeafd40955 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -101,7 +101,7 @@ installed. # \brief The Addon Manager allows users to install workbenches and macros made by other users # @{ - +INSTANCE = None class CommandAddonManager: """The main Addon Manager class and FreeCAD command""" @@ -156,6 +156,10 @@ class CommandAddonManager: self.update_all_worker = None self.developer_mode = None + # Give other parts of the AM access to the current instance + global INSTANCE + INSTANCE = self + def GetResources(self) -> Dict[str, str]: return { "Pixmap": "AddonManager", @@ -918,6 +922,7 @@ class CommandAddonManager: self.check_for_python_package_updates_worker.finished.connect( self.do_next_startup_phase ) + self.update_allowed_packages_list() # Not really the best place for it... self.check_for_python_package_updates_worker.start() def show_python_updates_dialog(self) -> None: @@ -1118,7 +1123,7 @@ class CommandAddonManager: False.""" bad_packages = [] - self.update_allowed_packages_list() + #self.update_allowed_packages_list() for dep in python_required: if dep not in self.allowed_packages: bad_packages.append(dep) diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index 747b3015b2..658beb7672 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -26,11 +26,15 @@ SET(AddonManager_SRCS dependency_resolution_dialog.ui developer_mode.ui developer_mode_add_content.ui + developer_mode_advanced_freecad_versions.ui developer_mode_copyright_info.ui developer_mode_dependencies.ui + developer_mode_edit_dependency.ui + developer_mode_freecad_versions.ui developer_mode_license.ui developer_mode_people.ui developer_mode_select_from_list.ui + developer_mode_tags.ui expanded_view.py first_run.ui Init.py diff --git a/src/Mod/AddonManager/addonmanager_devmode.py b/src/Mod/AddonManager/addonmanager_devmode.py index 9cc4a83a11..050ca28f08 100644 --- a/src/Mod/AddonManager/addonmanager_devmode.py +++ b/src/Mod/AddonManager/addonmanager_devmode.py @@ -39,6 +39,7 @@ from addonmanager_git import GitManager from addonmanager_devmode_license_selector import LicenseSelector from addonmanager_devmode_person_editor import PersonEditor +from addonmanager_devmode_add_content import AddContent translate = FreeCAD.Qt.translate @@ -217,7 +218,7 @@ class DeveloperMode: metadata = None if os.path.exists(metadata_path): try: - metadata = FreeCAD.Metadata(metadata_path) + self.metadata = FreeCAD.Metadata(metadata_path) except FreeCAD.Base.XMLBaseException as e: FreeCAD.Console.PrintError( translate( @@ -238,17 +239,17 @@ class DeveloperMode: + "\n\n" ) - if metadata: - self.dialog.displayNameLineEdit.setText(metadata.Name) - self.dialog.descriptionTextEdit.setPlainText(metadata.Description) - self.dialog.versionLineEdit.setText(metadata.Version) + if self.metadata: + self.dialog.displayNameLineEdit.setText(self.metadata.Name) + self.dialog.descriptionTextEdit.setPlainText(self.metadata.Description) + self.dialog.versionLineEdit.setText(self.metadata.Version) - self._populate_people_from_metadata(metadata) - self._populate_licenses_from_metadata(metadata) - self._populate_urls_from_metadata(metadata) - self._populate_contents_from_metadata(metadata) + self._populate_people_from_metadata(self.metadata) + self._populate_licenses_from_metadata(self.metadata) + self._populate_urls_from_metadata(self.metadata) + self._populate_contents_from_metadata(self.metadata) - self._populate_icon_from_metadata(metadata) + self._populate_icon_from_metadata(self.metadata) else: self._populate_without_metadata() @@ -461,6 +462,12 @@ class DeveloperMode: ) self.dialog.peopleTableWidget.itemDoubleClicked.connect(self._edit_person) + self.dialog.addContentItemToolButton.clicked.connect(self._add_content_clicked) + self.dialog.removeContentItemToolButton.clicked.connect(self._remove_content_clicked) + self.dialog.contentsListWidget.itemSelectionChanged.connect(self._content_selection_changed) + self.dialog.contentsListWidget.itemDoubleClicked.connect(self._edit_content) + + # Finally, populate the combo boxes, etc. self._populate_combo() if self.dialog.pathToAddonComboBox.currentIndex() != -1: @@ -611,3 +618,17 @@ class DeveloperMode: self.dialog.peopleTableWidget.removeRow(row) self._add_person_row(row, person_type, name, email) self.dialog.peopleTableWidget.selectRow(row) + + + def _add_content_clicked(self): + dlg = AddContent(self.current_mod, self.metadata) + dlg.exec() + + def _remove_content_clicked(self): + pass + + def _content_selection_changed(self): + pass + + def _edit_content(self, item): + pass diff --git a/src/Mod/AddonManager/addonmanager_devmode_add_content.py b/src/Mod/AddonManager/addonmanager_devmode_add_content.py index 7a1d4979ae..ca959e063c 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_add_content.py +++ b/src/Mod/AddonManager/addonmanager_devmode_add_content.py @@ -20,3 +20,490 @@ # * * # *************************************************************************** +""" Contains a class for adding a single content item, as well as auxilliary classes for +its dependent dialog boxes. """ + +import os +from typing import Optional, Tuple, List + +import FreeCAD +import FreeCADGui + +from Addon import INTERNAL_WORKBENCHES + +from PySide2.QtWidgets import QDialog, QLayout, QFileDialog, QTableWidgetItem +from PySide2.QtGui import QIcon +from PySide2.QtCore import Qt + +# pylint: disable=too-few-public-methods + +translate = FreeCAD.Qt.translate + + +class AddContent: + """A dialog for adding a single content item to the package metadata.""" + + def __init__(self, path_to_addon: os.PathLike, toplevel_metadata: FreeCAD.Metadata): + """path_to_addon is the full path to the toplevel directory of this Addon, and + toplevel_metadata is to overall package.xml Metadata object for this Addon. This + information is used to assist the use in filling out the dialog by providing + sensible default values.""" + self.dialog = FreeCADGui.PySideUic.loadUi( + os.path.join(os.path.dirname(__file__), "developer_mode_add_content.ui") + ) + # These are in alphabetical order in English, but their actual label may be translated in + # the GUI. Store their underlying type as user data. + self.dialog.addonKindComboBox.setItemData(0, "macro") + self.dialog.addonKindComboBox.setItemData(1, "preferencepack") + self.dialog.addonKindComboBox.setItemData(2, "workbench") + + self.toplevel_metadata = toplevel_metadata + self.metadata = None + self.path_to_addon = path_to_addon.replace("/", os.path.sep) + if self.path_to_addon[-1] != os.path.sep: + self.path_to_addon += ( + os.path.sep + ) # Make sure the path ends with a separator + + self.dialog.iconLabel.hide() # Until we have an icon to display + + self.dialog.iconBrowseButton.clicked.connect(self._browse_for_icon_clicked) + self.dialog.subdirectoryBrowseButton.clicked.connect( + self._browse_for_subdirectory_clicked + ) + self.dialog.tagsButton.clicked.connect(self._tags_clicked) + self.dialog.dependenciesButton.clicked.connect(self._dependencies_clicked) + self.dialog.freecadVersionsButton.clicked.connect( + self._freecad_versions_clicked + ) + + def exec( + self, + content_kind: str = "workbench", + metadata: FreeCAD.Metadata = None, + singleton: bool = True, + ) -> Optional[Tuple[str, FreeCAD.Metadata]]: + """Execute the dialog as a modal, returning a new Metadata object if the dialog + is accepted, or None if it is rejected. This metadata object represents a single + new content item. It's returned as a tuple with the object type as the first component, + and the metadata object itself as the second.""" + self.metadata = metadata + self.dialog.singletonCheckBox.setChecked(singleton) + if singleton: + # This doesn't happen automatically the first time + self.dialog.otherMetadataGroupBox.hide() + index = self.dialog.addonKindComboBox.findData(content_kind) + if index == -1: + index = 2 # Workbench + FreeCAD.Console.PrintWarning( + translate("AddonsInstaller", "Unrecognized content kind '{}'").format( + content_kind + ) + + "\n" + ) + self.dialog.addonKindComboBox.setCurrentIndex(index) + if metadata: + self._populate_dialog(metadata) + + self.dialog.layout().setSizeConstraint(QLayout.SetFixedSize) + result = self.dialog.exec() + if result == QDialog.Accepted: + return self._generate_metadata() + return None + + def _populate_dialog(self, metadata: FreeCAD.Metadata) -> None: + """Fill in the dialog with the details from the passed metadata object""" + addon_kind = self.dialog.addonKindComboBox.currentData() + if addon_kind == "workbench": + self.dialog.workbenchClassnameLineEdit.setText(metadata.Classname) + elif addon_kind == "macro": + pass + elif addon_kind == "preferencepack": + pass + else: + raise RuntimeError("Invalid data found for selection") + + # Now set the rest of it + if metadata.Icon: + self._set_icon(metadata.Icon) + elif self.toplevel_metadata.Icon: + if metadata.Subdirectory and metadata.Subdirectory != "./": + self._set_icon("../" + self.toplevel_metadata.Icon) + else: + self._set_icon(self.toplevel_metadata.Icon) + else: + self.dialog.iconLabel.hide() + self.dialog.iconLineEdit.setText("") + + if metadata.Subdirectory: + self.dialog.subdirectoryLineEdit.setText(metadata.Subdirectory) + else: + self.dialog.subdirectoryLineEdit.setText("") + + def _set_icon(self, icon_relative_path): + """Load the icon and display it, and its path, in the dialog.""" + icon_path = os.path.join( + self.path_to_addon, icon_relative_path.replace("/", os.path.sep) + ) + if os.path.isfile(icon_path): + icon_data = QIcon(icon_path) + if not icon_data.isNull(): + self.dialog.iconLabel.setPixmap(icon_data.pixmap(32, 32)) + self.dialog.iconLabel.show() + else: + FreeCAD.Console.PrintError( + translate("AddonsInstaller", "Unable to locate icon at {}").format( + icon_path + ) + + "\n" + ) + self.dialog.iconLineEdit.setText(icon_relative_path) + + def _generate_metadata(self) -> Tuple[str, FreeCAD.Metadata]: + """Create and return a new metadata object based on the contents of the dialog.""" + return ("workbench", FreeCAD.Metadata()) + + ############################################################################################### + # DIALOG SLOTS + ############################################################################################### + + def _browse_for_icon_clicked(self): + """Callback: when the "Browse..." button for the icon field is clicked""" + subdir = self.dialog.subdirectoryLineEdit.text() + start_dir = os.path.join(self.path_to_addon, subdir) + new_icon_path, _ = QFileDialog.getOpenFileName( + parent=self.dialog, + caption=translate( + "AddonsInstaller", + "Select an icon file for this content item", + ), + dir=start_dir, + ) + + base_path = self.path_to_addon.replace("/", os.path.sep) + icon_path = new_icon_path.replace("/", os.path.sep) + + if not icon_path.startswith(base_path): + FreeCAD.Console.PrintError( + translate("AddonsInstaller", "{} is not a subdirectory of {}").format( + icon_path, base_path + ) + + "\n" + ) + return + self._set_icon(new_icon_path[len(base_path) :]) + + def _browse_for_subdirectory_clicked(self): + """Callback: when the "Browse..." button for the subdirectory field is clicked""" + subdir = self.dialog.subdirectoryLineEdit.text() + start_dir = os.path.join(self.path_to_addon, subdir) + new_subdir_path = QFileDialog.getExistingDirectory( + parent=self.dialog, + caption=translate( + "AddonsInstaller", + "Select the subdirectory for this content item", + ), + dir=start_dir, + ) + if new_subdir_path[-1] != "/": + new_subdir_path += "/" + + # Three legal possibilities: + # 1) This might be the toplevel directory, in which case we want to set + # metadata.Subdirectory to "./" + # 2) This might be a subdirectory with the same name as the content item, in which case + # we don't need to set metadata.Subdirectory at all + # 3) This might be some other directory name, but still contained within the top-level + # directory, in which case we want to set metadata.Subdirectory to the relative path + + # First, reject anything that isn't within the appropriate directory structure: + base_path = self.path_to_addon.replace("/", os.path.sep) + subdir_path = new_subdir_path.replace("/", os.path.sep) + if not subdir_path.startswith(base_path): + FreeCAD.Console.PrintError( + translate("AddonsInstaller", "{} is not a subdirectory of {}").format( + subdir_path, base_path + ) + + "\n" + ) + return + + relative_path = subdir_path[len(base_path) :] + if not relative_path: + relative_path = "./" + elif relative_path[-1] == os.path.sep: + relative_path = relative_path[:-1] + self.dialog.subdirectoryLineEdit.setText(relative_path) + + def _tags_clicked(self): + """Show the tag editor""" + tags = [] + if self.metadata: + tags = self.metadata.Tag + dlg = EditTags(tags) + new_tags = dlg.exec() + + def _freecad_versions_clicked(self): + """Show the FreeCAD version editor""" + dlg = EditFreeCADVersions() + dlg.exec() + + def _dependencies_clicked(self): + """Show the dependencies editor""" + dlg = EditDependencies() + dlg.exec() + + +class EditTags: + """A dialog to edit tags""" + + def __init__(self, tags: List[str] = None): + self.dialog = FreeCADGui.PySideUic.loadUi( + os.path.join(os.path.dirname(__file__), "developer_mode_tags.ui") + ) + self.original_tags = tags + if tags: + self.dialog.lineEdit.setText(", ".join(tags)) + + def exec(self): + """Execute the dialog, returning a list of tags (which may be empty, but still represents + the expected list of tags to be set, e.g. the user may have removed them all).""" + result = self.dialog.exec() + if result == QDialog.Accepted: + new_tags: List[str] = self.dialog.lineEdit.text().split(",") + clean_tags: List[str] = [] + for tag in new_tags: + clean_tags.append(tag.strip()) + return clean_tags + return self.original_tags + + +class EditDependencies: + """A dialog to edit dependency information""" + + def __init__(self): + self.dialog = FreeCADGui.PySideUic.loadUi( + os.path.join(os.path.dirname(__file__), "developer_mode_dependencies.ui") + ) + self.dialog.addDependencyToolButton.setIcon( + QIcon.fromTheme("add", QIcon(":/icons/list-add.svg")) + ) + self.dialog.removeDependencyToolButton.setIcon( + QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg")) + ) + self.dialog.addDependencyToolButton.clicked.connect( + self._add_dependency_clicked + ) + self.dialog.removeDependencyToolButton.clicked.connect( + self._remove_dependency_clicked + ) + self.dialog.tableWidget.itemDoubleClicked.connect(self._edit_dependency) + self.dialog.tableWidget.itemSelectionChanged.connect( + self._current_index_changed + ) + + self.dialog.removeDependencyToolButton.setDisabled(True) + + def exec(self): + """Execute the dialog""" + self.dialog.exec() + + def _add_dependency_clicked(self): + """Callback: The add button was clicked""" + dlg = EditDependency() + dep_type, dep_name, dep_optional = dlg.exec() + row = self.dialog.tableWidget.rowCount() + self._add_row(row, dep_type, dep_name, dep_optional) + + def _add_row(self, row, dep_type, dep_name, dep_optional): + """Utility function to add a row to the table.""" + translations = { + "workbench": translate("AddonsInstaller", "Workbench"), + "addon": translate("AddonsInstaller", "Addon"), + "python": translate("AddonsInstaller", "Python"), + } + if dep_type and dep_name: + self.dialog.tableWidget.insertRow(row) + type_item = QTableWidgetItem(translations[dep_type]) + type_item.setData(Qt.UserRole, dep_type) + self.dialog.tableWidget.setItem(row, 0, type_item) + self.dialog.tableWidget.setItem(row, 1, QTableWidgetItem(dep_name)) + if dep_optional: + self.dialog.tableWidget.setItem( + row, 2, QTableWidgetItem(translate("AddonsInstaller", "Yes")) + ) + + def _remove_dependency_clicked(self): + """Callback: The remove button was clicked""" + items = self.dialog.tableWidget.selectedItems() + if items: + row = items[0].row() + self.dialog.tableWidget.removeRow(row) + + def _edit_dependency(self, item): + """Callback: the dependency was double-clicked""" + row = item.row() + dlg = EditDependency() + dep_type = self.dialog.tableWidget.item(row, 0).data(Qt.UserRole) + dep_name = self.dialog.tableWidget.item(row, 1).text() + dep_optional = bool(self.dialog.tableWidget.item(row, 2)) + dep_type, dep_name, dep_optional = dlg.exec(dep_type, dep_name, dep_optional) + if dep_type and dep_name: + self.dialog.tableWidget.removeRow(row) + self._add_row(row, dep_type, dep_name, dep_optional) + + def _current_index_changed(self): + if self.dialog.tableWidget.selectedItems(): + self.dialog.removeDependencyToolButton.setDisabled(False) + else: + self.dialog.removeDependencyToolButton.setDisabled(True) + + +class EditDependency: + """A dialog to edit a single piece of dependency information""" + + def __init__(self): + self.dialog = FreeCADGui.PySideUic.loadUi( + os.path.join(os.path.dirname(__file__), "developer_mode_edit_dependency.ui") + ) + + self.dialog.typeComboBox.addItem( + translate("AddonsInstaller", "Internal Workbench"), "workbench" + ) + self.dialog.typeComboBox.addItem( + translate("AddonsInstaller", "External Addon"), "addon" + ) + self.dialog.typeComboBox.addItem( + translate("AddonsInstaller", "Python Package"), "python" + ) + + self.dialog.typeComboBox.currentIndexChanged.connect( + self._type_selection_changed + ) + self.dialog.dependencyComboBox.currentIndexChanged.connect( + self._dependency_selection_changed + ) + + self.dialog.typeComboBox.setCurrentIndex( + 2 + ) # Expect mostly Python dependencies... + + def exec( + self, dep_type="", dep_name="", dep_optional=False + ) -> Tuple[str, str, bool]: + """Execute the dialog, returning a tuple of the type of dependency (workbench, addon, or + python), the name of the dependency, and a boolean indicating whether this is optional.""" + + # If we are editing an existing row, set up the dialog: + if dep_type and dep_name: + index = self.dialog.typeComboBox.findData(dep_type) + if index == -1: + raise RuntimeError(f"Invaid dependency type {dep_type}") + self.dialog.typeComboBox.setCurrentIndex(index) + index = self.dialog.dependencyComboBox.findData(dep_name) + if index == -1: + index = self.dialog.dependencyComboBox.findData("other") + self.dialog.dependencyComboBox.setCurrentIndex(index) + self.dialog.lineEdit.setText(dep_name) + self.dialog.optionalCheckBox.setChecked(dep_optional) + + # Run the dialog (modal) + result = self.dialog.exec() + if result == QDialog.Accepted: + dep_type = self.dialog.typeComboBox.currentData() + dep_optional = self.dialog.optionalCheckBox.isChecked() + dep_name = self.dialog.dependencyComboBox.currentData() + if dep_name == "other": + dep_name = self.dialog.lineEdit.text() + return (dep_type, dep_name, dep_optional) + return ("", "", False) + + def _populate_internal_workbenches(self): + """Add all known internal FreeCAD Workbenches to the list""" + self.dialog.dependencyComboBox.clear() + for display_name, name in INTERNAL_WORKBENCHES.items(): + self.dialog.dependencyComboBox.addItem(display_name, name) + # No "other" option is supported for this type of dependency + + def _populate_external_addons(self): + """Add all known addons to the list""" + self.dialog.dependencyComboBox.clear() + #pylint: disable=import-outside-toplevel + from AddonManager import INSTANCE as AM_INSTANCE + + repo_dict = {} + # We need a case-insensitive sorting of all repo types, displayed and sorted by their + # display name, but keeping track of their official name as well (stored in the UserRole) + for repo in AM_INSTANCE.item_model.repos: + repo_dict[repo.display_name.lower()] = (repo.display_name, repo.name) + sorted_keys = sorted(repo_dict.keys()) + for item in sorted_keys: + self.dialog.dependencyComboBox.addItem( + repo_dict[item][0], repo_dict[item][1] + ) + self.dialog.dependencyComboBox.addItem( + translate("AddonsInstaller", "Other..."), "other" + ) + + def _populate_allowed_python_packages(self): + """Add all allowed python packages to the list""" + self.dialog.dependencyComboBox.clear() + #pylint: disable=import-outside-toplevel + from AddonManager import INSTANCE as AM_INSTANCE + + packages = sorted(AM_INSTANCE.allowed_packages) + for package in packages: + self.dialog.dependencyComboBox.addItem(package, package) + self.dialog.dependencyComboBox.addItem( + translate("AddonsInstaller", "Other..."), "other" + ) + + def _type_selection_changed(self, _): + """Callback: The type of dependency has been changed""" + selection = self.dialog.typeComboBox.currentData() + if selection == "workbench": + self._populate_internal_workbenches() + elif selection == "addon": + self._populate_external_addons() + elif selection == "python": + self._populate_allowed_python_packages() + else: + raise RuntimeError("Invalid data found for selection") + + def _dependency_selection_changed(self, _): + selection = self.dialog.dependencyComboBox.currentData() + if selection == "other": + self.dialog.lineEdit.show() + else: + self.dialog.lineEdit.hide() + + +class EditFreeCADVersions: + """A dialog to edit minimum and maximum FreeCAD version support""" + + def __init__(self): + self.dialog = FreeCADGui.PySideUic.loadUi( + os.path.join( + os.path.dirname(__file__), "developer_mode_freecad_versions.ui" + ) + ) + + def exec(self): + """Execute the dialog""" + self.dialog.exec() + + +class EditAdvancedVersions: + """A dialog to support mapping specific git branches, tags, or commits to specific + versions of FreeCAD.""" + + def __init__(self): + self.dialog = FreeCADGui.PySideUic.loadUi( + os.path.join( + os.path.dirname(__file__), "developer_mode_advanced_freecad_versions.ui" + ) + ) + + def exec(self): + """Execute the dialog""" + self.dialog.exec() diff --git a/src/Mod/AddonManager/developer_mode_add_content.ui b/src/Mod/AddonManager/developer_mode_add_content.ui index deea6a3b97..21b5b6dfa7 100644 --- a/src/Mod/AddonManager/developer_mode_add_content.ui +++ b/src/Mod/AddonManager/developer_mode_add_content.ui @@ -1,75 +1,191 @@ - Dialog - + addContentDialog + 0 0 - 641 - 388 + 642 + 593 - Dialog + Content Item - - - If this is the only thing in the Addon, all other metadata can be inherited from the top level, and does not need to be specified here. - - - This workbench is the only item in the Addon - - - true + + + + + Content type: + + + + + + + + Macro + + + + + Preference Pack + + + + + Workbench + + + + + + + + If this is the only thing in the Addon, all other metadata can be inherited from the top level, and does not need to be specified here. + + + This is the only item in the Addon + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 1 + + + + + + Main macro file + + + + + + + The file with the macro's metadata in it + + + + + + + Browse... + + + + + + + + + + + Preference Pack Name + + + + + + + + + + + + + + Workbench class name + + + + + + + Class that defines "Icon" data member + + + + + - + - Class name + Subdirectory - + + + + + Optional, defaults to name of content item + + + + + + + Browse... + + + + - - - Path - - - - - - - Icon - - + + - + actualIcon - + + + Optional, defaults to inheriting from top-level Addon + + - + Browse... @@ -82,14 +198,21 @@ - + + + Tags... + + + + + Dependencies... - + FreeCAD Versions... @@ -111,207 +234,304 @@ - + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + Other Metadata - - - - License file path - - - - - - - Explanation of what this Addon provides. Displayed in the Addon Manager. It is not necessary for this to state that this is a FreeCAD Addon. - - - - - - - - - - - - - - - - - - - - - - Browse... - - - - - - - Create... - - - - - - - - - - - Primary maintainer name - - - - - - - - - - Contact email - - - - - - - - - - Add additional people - - - - - - - - - Version - - - - + Displayed in the Addon Manager's list of Addons. Should not include the word "FreeCAD". - - - - License + + + + true - + + + + Version + + + + Description + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + - + - - - - Semantic (e.g. 1.2.3-beta) - - - - - CalVer (e.g. 2021.12.08) - - - - - - - - Y: - - - - - - - - - - M: - - - - - - - - - - D: - - - - - - - - - - - - - - - - + - (optional suffix) + Semantic (1.2.3-beta) or CalVer (2022.08.30) styles supported - Set to today + Set to today (CalVer style) - + Display Name + + + + + + + 2 + 0 + + + + People + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 3 + + + true + + + 75 + + + true + + + false + + + + Kind + + + + + Name + + + + + Email + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + true + + + + + + + ... + + + true + + + + + + + + + + + + + 1 + 0 + + + + + 150 + 0 + + + + + 0 + 0 + + + + Licenses + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 2 + + + true + + + 60 + + + true + + + false + + + + License + + + + + License file + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + true + + + + + + + ... + + + true + + + + + + + + + + + + + + + 50 + true + false + + + + Any fields left blank are inherited from the top-level Addon metadata, so technically they are all optional. For Addons with multiple content items, each item should provide a unique Display Name and Description. + + + true + + + - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -329,7 +549,7 @@ buttonBox accepted() - Dialog + addContentDialog accept() @@ -345,7 +565,7 @@ buttonBox rejected() - Dialog + addContentDialog reject() @@ -358,5 +578,37 @@ + + addonKindComboBox + currentIndexChanged(int) + stackedWidget + setCurrentIndex(int) + + + 134 + 19 + + + 320 + 83 + + + + + singletonCheckBox + toggled(bool) + otherMetadataGroupBox + setHidden(bool) + + + 394 + 19 + + + 320 + 294 + + + diff --git a/src/Mod/AddonManager/developer_mode_advanced_freecad_versions.ui b/src/Mod/AddonManager/developer_mode_advanced_freecad_versions.ui new file mode 100644 index 0000000000..e3de761ed6 --- /dev/null +++ b/src/Mod/AddonManager/developer_mode_advanced_freecad_versions.ui @@ -0,0 +1,131 @@ + + + FreeCADVersionToBranchMapDialog + + + + 0 + 0 + 400 + 300 + + + + Advanced Version Mapping + + + + + + Upcoming versions of the FreeCAD Addon Manager will support developers' setting a specific branch or tag for use with a specific version of FreeCAD (e.g. setting a specific tag as the last version of your Addon to support v0.19, etc.) + + + true + + + + + + + true + + + QAbstractItemView::SelectRows + + + false + + + true + + + + FreeCAD Version + + + + + Best-available branch, tag, or commit + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + + + + ... + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + FreeCADVersionToBranchMapDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FreeCADVersionToBranchMapDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/AddonManager/developer_mode_dependencies.ui b/src/Mod/AddonManager/developer_mode_dependencies.ui index 2a03d68b5e..a1d11f65a2 100644 --- a/src/Mod/AddonManager/developer_mode_dependencies.ui +++ b/src/Mod/AddonManager/developer_mode_dependencies.ui @@ -1,191 +1,83 @@ - AddDependenciesDialog - + DependencyDialog + 0 0 - 743 - 221 + 348 + 242 - Addon Dependencies + Dependencies - + - + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + + Dependency type + + + + + Name + + + + + Optional? + + + + + + - - - FreeCAD Workbenches + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - ... - - - - - - - ... - - - - - - - - - External Addons + + + ... - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - ... - - - - - - - ... - - - - - - - - - - - - Required Python Packages - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - ... - - - - - - - ... - - - - - - - - - - - - Optional Python Packages - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - ... - - - - - - - ... - - - - - - @@ -207,7 +99,7 @@ buttonBox accepted() - AddDependenciesDialog + DependencyDialog accept() @@ -223,7 +115,7 @@ buttonBox rejected() - AddDependenciesDialog + DependencyDialog reject() diff --git a/src/Mod/AddonManager/developer_mode_edit_dependency.ui b/src/Mod/AddonManager/developer_mode_edit_dependency.ui new file mode 100644 index 0000000000..d3f8662186 --- /dev/null +++ b/src/Mod/AddonManager/developer_mode_edit_dependency.ui @@ -0,0 +1,105 @@ + + + EditDependencyDialog + + + + 0 + 0 + 347 + 142 + + + + Edit Dependency + + + + + + Dependency Type + + + + + + + + + + Dependency + + + + + + + + + + + + Package name, if "Other..." + + + + + + + + + If this is an optional dependency, the Addon Manager will offer to install it (when possible), but will not block installation if the user chooses not to, or cannot, instal the package. + + + Optional + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + EditDependencyDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditDependencyDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/AddonManager/developer_mode_freecad_versions.ui b/src/Mod/AddonManager/developer_mode_freecad_versions.ui new file mode 100644 index 0000000000..7716c27664 --- /dev/null +++ b/src/Mod/AddonManager/developer_mode_freecad_versions.ui @@ -0,0 +1,99 @@ + + + FreeCADVersionsDialog + + + + 0 + 0 + 400 + 120 + + + + Supported FreeCAD Versions + + + + + + Minimum FreeCAD Version Supported + + + + + + + Optional + + + + + + + Maximum FreeCAD Version Supported + + + + + + + Optional + + + + + + + Advanced version mapping... + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + FreeCADVersionsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FreeCADVersionsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/AddonManager/developer_mode_tags.ui b/src/Mod/AddonManager/developer_mode_tags.ui new file mode 100644 index 0000000000..f149afb3dc --- /dev/null +++ b/src/Mod/AddonManager/developer_mode_tags.ui @@ -0,0 +1,86 @@ + + + Dialog + + + + 0 + 0 + 400 + 106 + + + + Edit Tags + + + + + + Comma-separated list of tags describing this item: + + + + + + + + + + + true + + + + HINT: Common tags include "Assembly", "FEM", "Mesh", "NURBS", etc. + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +