diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index a1721dc342..747b3015b2 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -11,6 +11,7 @@ SET(AddonManager_SRCS
addonmanager_devmode_add_content.py
addonmanager_devmode_license_selector.py
addonmanager_devmode_dependencies.py
+ addonmanager_devmode_person_editor.py
addonmanager_git.py
addonmanager_macro.py
addonmanager_utilities.py
@@ -28,6 +29,7 @@ SET(AddonManager_SRCS
developer_mode_copyright_info.ui
developer_mode_dependencies.ui
developer_mode_license.ui
+ developer_mode_people.ui
developer_mode_select_from_list.ui
expanded_view.py
first_run.ui
diff --git a/src/Mod/AddonManager/addonmanager_devmode.py b/src/Mod/AddonManager/addonmanager_devmode.py
index 41368c4654..9cc4a83a11 100644
--- a/src/Mod/AddonManager/addonmanager_devmode.py
+++ b/src/Mod/AddonManager/addonmanager_devmode.py
@@ -23,22 +23,28 @@
""" Classes to manage "Developer Mode" """
import os
-from typing import Optional
import FreeCAD
import FreeCADGui
-from PySide2.QtWidgets import QFileDialog, QTableWidgetItem, QDialog
-from PySide2.QtGui import QIcon, QValidator, QRegularExpressionValidator, QPixmap, QDesktopServices
-from PySide2.QtCore import QRegularExpression, QUrl, QFile, QIODevice
+from PySide2.QtWidgets import QFileDialog, QTableWidgetItem
+from PySide2.QtGui import (
+ QIcon,
+ QValidator,
+ QRegularExpressionValidator,
+ QPixmap,
+)
+from PySide2.QtCore import QRegularExpression, Qt
from addonmanager_git import GitManager
from addonmanager_devmode_license_selector import LicenseSelector
+from addonmanager_devmode_person_editor import PersonEditor
translate = FreeCAD.Qt.translate
# pylint: disable=too-few-public-methods
+
class AddonGitInterface:
"""Wrapper to handle the git calls needed by this class"""
@@ -153,6 +159,13 @@ class DeveloperMode:
"""The main Developer Mode dialog, for editing package.xml metadata graphically."""
def __init__(self):
+
+ # In the UI we want to show a translated string for the person type, but the underlying
+ # string must be the one expected by the metadata parser, in English
+ self.person_type_translation = {
+ "maintainer": translate("AddonsInstaller", "Maintainer"),
+ "author": translate("AddonsInstaller", "Author"),
+ }
self.dialog = FreeCADGui.PySideUic.loadUi(
os.path.join(os.path.dirname(__file__), "developer_mode.ui")
)
@@ -246,22 +259,12 @@ class DeveloperMode:
for maintainer in metadata.Maintainer:
name = maintainer["name"]
email = maintainer["email"]
- self.dialog.peopleTableWidget.insertRow(row)
- self.dialog.peopleTableWidget.setItem(
- row, 0, QTableWidgetItem(translate("AddonsInstaller", "Maintainer"))
- )
- self.dialog.peopleTableWidget.setItem(row, 1, QTableWidgetItem(name))
- self.dialog.peopleTableWidget.setItem(row, 2, QTableWidgetItem(email))
+ self._add_person_row(row, "maintainer", name, email)
row += 1
for author in metadata.Author:
name = author["name"]
email = author["email"]
- self.dialog.peopleTableWidget.insertRow(row)
- self.dialog.peopleTableWidget.setItem(
- row, 0, QTableWidgetItem(translate("AddonsInstaller", "Author"))
- )
- self.dialog.peopleTableWidget.setItem(row, 1, QTableWidgetItem(name))
- self.dialog.peopleTableWidget.setItem(row, 2, QTableWidgetItem(email))
+ self._add_person_row(row, "author", name, email)
row += 1
if row == 0:
@@ -273,6 +276,15 @@ class DeveloperMode:
+ "\n"
)
+ def _add_person_row(self, row, person_type, name, email):
+ """Add this person to the peopleTableWidget at row given"""
+ self.dialog.peopleTableWidget.insertRow(row)
+ item = QTableWidgetItem(self.person_type_translation[person_type])
+ item.setData(Qt.UserRole, person_type)
+ self.dialog.peopleTableWidget.setItem(row, 0, item)
+ self.dialog.peopleTableWidget.setItem(row, 1, QTableWidgetItem(name))
+ self.dialog.peopleTableWidget.setItem(row, 2, QTableWidgetItem(email))
+
def _populate_licenses_from_metadata(self, metadata):
"""Use the passed metadata object to populate the licenses"""
self.dialog.licensesTableWidget.setRowCount(0)
@@ -432,12 +444,22 @@ class DeveloperMode:
self.dialog.pathToAddonComboBox.editTextChanged.connect(
self._addon_combo_text_changed
)
+
self.dialog.addLicenseToolButton.clicked.connect(self._add_license_clicked)
- self.dialog.removeLicenseToolButton.clicked.connect(self._remove_license_clicked)
+ self.dialog.removeLicenseToolButton.clicked.connect(
+ self._remove_license_clicked
+ )
+ self.dialog.licensesTableWidget.itemSelectionChanged.connect(
+ self._license_selection_changed
+ )
+ self.dialog.licensesTableWidget.itemDoubleClicked.connect(self._edit_license)
+
self.dialog.addPersonToolButton.clicked.connect(self._add_person_clicked)
self.dialog.removePersonToolButton.clicked.connect(self._remove_person_clicked)
- self.dialog.peopleTableWidget.itemSelectionChanged.connect(self._person_selection_changed)
- self.dialog.licensesTableWidget.itemSelectionChanged.connect(self._license_selection_changed)
+ self.dialog.peopleTableWidget.itemSelectionChanged.connect(
+ self._person_selection_changed
+ )
+ self.dialog.peopleTableWidget.itemDoubleClicked.connect(self._edit_person)
# Finally, populate the combo boxes, etc.
self._populate_combo()
@@ -552,8 +574,21 @@ class DeveloperMode:
# the first entry
self.dialog.licensesTableWidget.removeRow(items[0].row())
+ def _edit_license(self, item):
+ row = item.row()
+ short_code = self.dialog.licensesTableWidget.item(row, 0).text()
+ path = self.dialog.licensesTableWidget.item(row, 1).text()
+ license_selector = LicenseSelector(self.current_mod)
+ short_code, path = license_selector.exec(short_code, path)
+ if short_code:
+ self.dialog.licensesTableWidget.removeRow(row)
+ self._add_license_row(row, short_code, path)
+
def _add_person_clicked(self):
- pass
+ dlg = PersonEditor()
+ person_type, name, email = dlg.exec()
+ if person_type and name:
+ self._add_person_row(row, person_type, name, email)
def _remove_person_clicked(self):
items = self.dialog.peopleTableWidget.selectedIndexes()
@@ -562,3 +597,17 @@ class DeveloperMode:
# the first entry
self.dialog.peopleTableWidget.removeRow(items[0].row())
+ def _edit_person(self, item):
+ row = item.row()
+ person_type = self.dialog.peopleTableWidget.item(row, 0).data(Qt.UserRole)
+ name = self.dialog.peopleTableWidget.item(row, 1).text()
+ email = self.dialog.peopleTableWidget.item(row, 2).text()
+
+ dlg = PersonEditor()
+ dlg.setup(person_type, name, email)
+ person_type, name, email = dlg.exec()
+
+ if person_type and name:
+ self.dialog.peopleTableWidget.removeRow(row)
+ self._add_person_row(row, person_type, name, email)
+ self.dialog.peopleTableWidget.selectRow(row)
diff --git a/src/Mod/AddonManager/addonmanager_devmode_license_selector.py b/src/Mod/AddonManager/addonmanager_devmode_license_selector.py
index 4f6643d5a8..5a9bcb37be 100644
--- a/src/Mod/AddonManager/addonmanager_devmode_license_selector.py
+++ b/src/Mod/AddonManager/addonmanager_devmode_license_selector.py
@@ -114,15 +114,14 @@ class LicenseSelector:
short_code = self.pref.GetString("devModeLastSelectedLicense", "LGPLv2.1")
self.set_license(short_code)
- def exec(self, short_code: str = None, license_path: str = None) -> Optional[str]:
+ def exec(self, short_code: str = None, license_path: str = "") -> Optional[str]:
"""The main method for executing this dialog, as a modal that returns a tuple of the
license's "short code" and optionally the path to the license file. Returns a tuple
of None,None if the user cancels the operation."""
if short_code:
self.set_license(short_code)
- if license_path:
- self.dialog.pathLineEdit.setText(license_path)
+ self.dialog.pathLineEdit.setText(license_path)
result = self.dialog.exec()
if result == QDialog.Accepted:
new_short_code = self.dialog.comboBox.currentData()
@@ -188,17 +187,20 @@ class LicenseSelector:
dir=start_dir,
)
if license_path:
- self._set_path(start_dir, license_path)
+ self._set_path(self.path_to_addon, license_path)
def _set_path(self, start_dir: str, license_path: str):
"""Sets the value displayed in the path widget to the relative path from
start_dir to license_path"""
license_path = license_path.replace("/", os.path.sep)
- if not license_path.startswith(start_dir.replace("/", os.path.sep)):
+ base_dir = start_dir.replace("/", os.path.sep)
+ if base_dir[-1] != os.path.sep:
+ base_dir += os.path.sep
+ if not license_path.startswith(base_dir):
FreeCAD.Console.PrintError("Selected file not in Addon\n")
- # Eventually offer to copy it...
+ # Eventually offer to copy it?
return
- relative_path = license_path[len(start_dir) :]
+ relative_path = license_path[len(base_dir) :]
relative_path = relative_path.replace(os.path.sep, "/")
self.dialog.pathLineEdit.setText(relative_path)
diff --git a/src/Mod/AddonManager/addonmanager_devmode_person_editor.py b/src/Mod/AddonManager/addonmanager_devmode_person_editor.py
new file mode 100644
index 0000000000..6efcd8dbf8
--- /dev/null
+++ b/src/Mod/AddonManager/addonmanager_devmode_person_editor.py
@@ -0,0 +1,77 @@
+# ***************************************************************************
+# * *
+# * Copyright (c) 2022 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 *
+# * . *
+# * *
+# ***************************************************************************
+
+""" Contains a class to handle editing a person (from a Metadata standpoint). """
+
+import os
+from typing import Tuple # Needed until Py 3.9, when tuple supports this directly
+
+from PySide2.QtWidgets import QDialog
+
+import FreeCAD
+import FreeCADGui
+
+translate = FreeCAD.Qt.translate
+
+
+class PersonEditor:
+ """Create or edit a maintainer or author record."""
+
+ def __init__(self):
+
+ self.dialog = FreeCADGui.PySideUic.loadUi(
+ os.path.join(os.path.dirname(__file__), "developer_mode_people.ui")
+ )
+ self.dialog.comboBox.clear()
+ self.dialog.comboBox.addItem(
+ translate("AddonsInstaller", "Maintainer"), userData="maintainer"
+ )
+ self.dialog.comboBox.addItem(
+ translate("AddonsInstaller", "Author"), userData="author"
+ )
+
+ def exec(self) -> Tuple[str, str, str]:
+ """Run the dialog, and return a tuple of the person's record type, their name, and their
+ email address. Email may be None. If the others are None it's because the user cancelled
+ the interaction."""
+ result = self.dialog.exec()
+ if result == QDialog.Accepted:
+ return (
+ self.dialog.comboBox.currentData(),
+ self.dialog.nameLineEdit.text(),
+ self.dialog.emailLineEdit.text(),
+ )
+ return (None, None, None)
+
+ def setup(
+ self, person_type: str = "maintainer", name: str = "", email: str = ""
+ ) -> None:
+ """Configure the dialog"""
+ index = self.dialog.comboBox.findData(person_type)
+ if index == -1:
+ FreeCAD.Console.PrintWarning(
+ f"Internal Error: unrecognized person type {person_type}"
+ )
+ index = 0
+ self.dialog.comboBox.setCurrentIndex(index)
+ self.dialog.nameLineEdit.setText(name)
+ self.dialog.emailLineEdit.setText(email)
diff --git a/src/Mod/AddonManager/developer_mode_people.ui b/src/Mod/AddonManager/developer_mode_people.ui
new file mode 100644
index 0000000000..1dc0c828fb
--- /dev/null
+++ b/src/Mod/AddonManager/developer_mode_people.ui
@@ -0,0 +1,99 @@
+
+
+ personDialog
+
+
+
+ 0
+ 0
+ 313
+ 118
+
+
+
+ Add Person
+
+
+ -
+
+
+ A maintainer is someone with current commit access on this project. An author is anyone else you'd like to give credit to.
+
+
+
+ -
+
+
-
+
+
+ Name:
+
+
+
+ -
+
+
+ -
+
+
+ Email:
+
+
+
+ -
+
+
+ Email is required for maintainers, and optional for authors.
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ personDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ personDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+