diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py
index aeafd40955..15421f4fe3 100644
--- a/src/Mod/AddonManager/AddonManager.py
+++ b/src/Mod/AddonManager/AddonManager.py
@@ -32,7 +32,7 @@ import tempfile
import hashlib
import threading
import json
-import re # Needed for py 3.6 and earlier, can remove later, search for "re."
+import re # Needed for py 3.6 and earlier, can remove later, search for "re."
from datetime import date, timedelta
from typing import Dict, List
@@ -75,9 +75,12 @@ from addonmanager_devmode import DeveloperMode
import NetworkManager
translate = FreeCAD.Qt.translate
+
+
def QT_TRANSLATE_NOOP(_, txt):
return txt
+
__title__ = "FreeCAD Addon Manager Module"
__author__ = "Yorik van Havre", "Jonathan Wiedemann", "Kurt Kremitzki", "Chris Hennes"
__url__ = "http://www.freecad.org"
@@ -103,6 +106,7 @@ installed.
INSTANCE = None
+
class CommandAddonManager:
"""The main Addon Manager class and FreeCAD command"""
@@ -381,9 +385,7 @@ class CommandAddonManager:
self.dialog.buttonUpdateDependencies.clicked.connect(
self.show_python_updates_dialog
)
- self.dialog.buttonDevTools.clicked.connect(
- self.show_developer_tools
- )
+ self.dialog.buttonDevTools.clicked.connect(self.show_developer_tools)
self.packageList.itemSelected.connect(self.table_row_activated)
self.packageList.setEnabled(False)
self.packageDetails.execute.connect(self.executemacro)
@@ -624,10 +626,12 @@ class CommandAddonManager:
self.startup_sequence.append(self.load_macro_metadata)
selection = pref.GetString("SelectedAddon", "")
if selection:
- self.startup_sequence.insert(2, functools.partial(self.select_addon, selection))
+ self.startup_sequence.insert(
+ 2, functools.partial(self.select_addon, selection)
+ )
pref.SetString("SelectedAddon", "")
# TODO: migrate this to the developer mode tools
- #if ADDON_MANAGER_DEVELOPER_MODE:
+ # if ADDON_MANAGER_DEVELOPER_MODE:
# self.startup_sequence.append(self.validate)
self.current_progress_region = 0
self.number_of_progress_regions = len(self.startup_sequence)
@@ -922,7 +926,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.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:
@@ -931,7 +935,7 @@ class CommandAddonManager:
self.manage_python_packages_dialog.show()
def show_developer_tools(self) -> None:
- """ Display the developer tools dialog """
+ """Display the developer tools dialog"""
if not self.developer_mode:
self.developer_mode = DeveloperMode()
self.developer_mode.show()
@@ -1123,7 +1127,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)
@@ -1227,7 +1231,9 @@ class CommandAddonManager:
).clicked.connect(functools.partial(self.dependency_dialog_yes_clicked, repo))
self.dependency_dialog.buttonBox.button(
QtWidgets.QDialogButtonBox.Ignore
- ).clicked.connect(functools.partial(self.dependency_dialog_ignore_clicked, repo))
+ ).clicked.connect(
+ functools.partial(self.dependency_dialog_ignore_clicked, repo)
+ )
self.dependency_dialog.buttonBox.button(
QtWidgets.QDialogButtonBox.Cancel
).setDefault(True)
@@ -1302,7 +1308,9 @@ class CommandAddonManager:
self.dependency_installation_worker.failure.connect(
self.dependency_installation_failure
)
- self.dependency_installation_worker.success.connect(functools.partial(self.install,installing_repo))
+ self.dependency_installation_worker.success.connect(
+ functools.partial(self.install, installing_repo)
+ )
self.dependency_installation_dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Information,
translate("AddonsInstaller", "Installing dependencies"),
@@ -1465,12 +1473,8 @@ class CommandAddonManager:
self.update_all_worker = UpdateAllWorker(self.packages_with_updates)
self.update_all_worker.progress_made.connect(self.update_progress_bar)
self.update_all_worker.status_message.connect(self.show_information)
- self.update_all_worker.success.connect(
- self.subupdates_succeeded.append
- )
- self.update_all_worker.failure.connect(
- self.subupdates_failed.append
- )
+ self.update_all_worker.success.connect(self.subupdates_succeeded.append)
+ self.update_all_worker.failure.connect(self.subupdates_failed.append)
self.update_all_worker.finished.connect(self.on_update_all_completed)
self.update_all_worker.start()
diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index 6e7fd0480e..6c60c00e3d 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -11,7 +11,9 @@ SET(AddonManager_SRCS
addonmanager_devmode_add_content.py
addonmanager_devmode_dependencies.py
addonmanager_devmode_license_selector.py
+ addonmanager_devmode_licenses_table.py
addonmanager_devmode_person_editor.py
+ addonmanager_devmode_people_table.py
addonmanager_devmode_predictor.py
addonmanager_devmode_validators.py
addonmanager_git.py
@@ -34,7 +36,9 @@ SET(AddonManager_SRCS
developer_mode_edit_dependency.ui
developer_mode_freecad_versions.ui
developer_mode_license.ui
+ developer_mode_licenses_table.ui
developer_mode_people.ui
+ developer_mode_people_table.ui
developer_mode_select_from_list.ui
developer_mode_tags.ui
expanded_view.py
diff --git a/src/Mod/AddonManager/addonmanager_devmode.py b/src/Mod/AddonManager/addonmanager_devmode.py
index 66a0164930..98b1f780e2 100644
--- a/src/Mod/AddonManager/addonmanager_devmode.py
+++ b/src/Mod/AddonManager/addonmanager_devmode.py
@@ -23,11 +23,18 @@
""" Classes to manage "Developer Mode" """
import os
+import datetime
import FreeCAD
import FreeCADGui
-from PySide2.QtWidgets import QFileDialog, QTableWidgetItem, QListWidgetItem, QDialog
+from PySide2.QtWidgets import (
+ QFileDialog,
+ QTableWidgetItem,
+ QListWidgetItem,
+ QDialog,
+ QSizePolicy,
+)
from PySide2.QtGui import (
QIcon,
QPixmap,
@@ -35,10 +42,11 @@ from PySide2.QtGui import (
from PySide2.QtCore import Qt
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
from addonmanager_devmode_validators import NameValidator, VersionValidator
+from addonmanager_devmode_predictor import Predictor
+from addonmanager_devmode_people_table import PeopleTable
+from addonmanager_devmode_licenses_table import LicensesTable
translate = FreeCAD.Qt.translate
@@ -47,6 +55,7 @@ translate = FreeCAD.Qt.translate
ContentTypeRole = Qt.UserRole
ContentIndexRole = Qt.UserRole + 1
+
class AddonGitInterface:
"""Wrapper to handle the git calls needed by this class"""
@@ -82,7 +91,6 @@ class AddonGitInterface:
return []
-
class DeveloperMode:
"""The main Developer Mode dialog, for editing package.xml metadata graphically."""
@@ -97,6 +105,20 @@ class DeveloperMode:
self.dialog = FreeCADGui.PySideUic.loadUi(
os.path.join(os.path.dirname(__file__), "developer_mode.ui")
)
+ self.people_table = PeopleTable()
+ self.licenses_table = LicensesTable()
+ large_size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ large_size_policy.setHorizontalStretch(2)
+ small_size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ small_size_policy.setHorizontalStretch(1)
+ self.people_table.widget.setSizePolicy(large_size_policy)
+ self.licenses_table.widget.setSizePolicy(small_size_policy)
+ self.dialog.peopleAndLicenseshorizontalLayout.addWidget(
+ self.people_table.widget
+ )
+ self.dialog.peopleAndLicenseshorizontalLayout.addWidget(
+ self.licenses_table.widget
+ )
self.pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
self.current_mod: str = ""
self.git_interface = None
@@ -108,18 +130,6 @@ class DeveloperMode:
self.dialog.displayNameLineEdit.setValidator(NameValidator())
self.dialog.versionLineEdit.setValidator(VersionValidator())
- self.dialog.addPersonToolButton.setIcon(
- QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
- )
- self.dialog.removePersonToolButton.setIcon(
- QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
- )
- self.dialog.addLicenseToolButton.setIcon(
- QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
- )
- self.dialog.removeLicenseToolButton.setIcon(
- QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
- )
self.dialog.addContentItemToolButton.setIcon(
QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
)
@@ -127,21 +137,34 @@ class DeveloperMode:
QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
)
- def show(self, parent=None):
+ def show(self, parent=None, path=None):
"""Show the main dev mode dialog"""
if parent:
self.dialog.setParent(parent)
+ if path and os.path.exists(path):
+ self.dialog.pathToAddonComboBox.setEditText(path)
+ elif self.pref.HasGroup("recentModsList"):
+ recent_mods_group = self.pref.GetGroup("recentModsList")
+ entry = recent_mods_group.GetString("Mod0", "")
+ if entry:
+ self._populate_dialog(entry)
+ self._update_recent_mods(entry)
+ self._populate_combo()
+ else:
+ self._clear_all_fields()
+ else:
+ self._clear_all_fields()
+
result = self.dialog.exec()
if result == QDialog.Accepted:
self._sync_metadata_to_ui()
+ self.metadata.write(os.path.join(self.current_mod, "package.xml"))
def _populate_dialog(self, path_to_repo):
"""Populate this dialog using the best available parsing of the contents of the repo at
path_to_repo. This is a multi-layered process that starts with any existing package.xml
file or other known metadata files, and proceeds through examining the contents of the
directory structure."""
- if self.current_mod == path_to_repo:
- return
self.current_mod = path_to_repo
self._scan_for_git_info(self.current_mod)
@@ -178,78 +201,13 @@ class DeveloperMode:
self.dialog.descriptionTextEdit.setPlainText(self.metadata.Description)
self.dialog.versionLineEdit.setText(self.metadata.Version)
- 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(self.metadata)
- def _populate_people_from_metadata(self, metadata):
- """Use the passed metadata object to populate the maintainers and authors"""
- self.dialog.peopleTableWidget.setRowCount(0)
- row = 0
- for maintainer in metadata.Maintainer:
- name = maintainer["name"]
- email = maintainer["email"]
- self._add_person_row(row, "maintainer", name, email)
- row += 1
- for author in metadata.Author:
- name = author["name"]
- email = author["email"]
- self._add_person_row(row, "author", name, email)
- row += 1
-
- if row == 0:
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "WARNING: No maintainer data found in metadata file.",
- )
- + "\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)
- row = 0
- for lic in metadata.License:
- name = lic["name"]
- path = lic["file"]
- self._add_license_row(row, name, path)
- row += 1
- if row == 0:
- FreeCAD.Console.PrintWarning(
- translate(
- "AddonsInstaller",
- "WARNING: No license data found in metadata file",
- )
- + "\n"
- )
-
- def _add_license_row(self, row: int, name: str, path: str):
- """ Add a row to the table of licenses """
- self.dialog.licensesTableWidget.insertRow(row)
- self.dialog.licensesTableWidget.setItem(row, 0, QTableWidgetItem(name))
- self.dialog.licensesTableWidget.setItem(row, 1, QTableWidgetItem(path))
- full_path = os.path.join(self.current_mod, path)
- if not os.path.isfile(full_path):
- FreeCAD.Console.PrintError(
- translate(
- "AddonsInstaller",
- "ERROR: Could not locate license file at {}",
- ).format(full_path)
- + "\n"
- )
+ self.people_table.show(self.metadata)
+ self.licenses_table.show(self.metadata, self.current_mod)
def _populate_urls_from_metadata(self, metadata):
"""Use the passed metadata object to populate the urls"""
@@ -312,7 +270,7 @@ class DeveloperMode:
)
contents_string += ", ".join(info)
- item = QListWidgetItem (contents_string)
+ item = QListWidgetItem(contents_string)
item.setData(ContentTypeRole, content_type)
item.setData(ContentIndexRole, counter)
self.dialog.contentsListWidget.addItem(item)
@@ -329,7 +287,7 @@ class DeveloperMode:
else:
self.has_toplevel_icon = False
contents = metadata.Content
- if contents["workbench"]:
+ if "workbench" in contents:
for wb in contents["workbench"]:
icon = wb.Icon
path = wb.Subdirectory
@@ -339,7 +297,7 @@ class DeveloperMode:
)
break
- if os.path.isfile(icon_path):
+ if icon_path and os.path.isfile(icon_path):
icon_data = QIcon(icon_path)
if not icon_data.isNull():
self.dialog.iconDisplayLabel.setPixmap(icon_data.pixmap(32, 32))
@@ -348,13 +306,8 @@ class DeveloperMode:
def _predict_metadata(self):
"""If there is no metadata, try to guess at values for it"""
self.metadata = FreeCAD.Metadata()
- self._predict_author_info()
- self._predict_name()
- self._predict_description()
- self._predict_contents()
- self._predict_icon()
- self._predict_urls()
- self._predict_license()
+ predictor = Predictor()
+ self.metadata = predictor.predict_metadata(self.current_mod)
def _scan_for_git_info(self, path):
"""Look for branch availability"""
@@ -378,59 +331,6 @@ class DeveloperMode:
self.dialog.documentationURLLineEdit.clear()
self.dialog.iconDisplayLabel.setPixmap(QPixmap())
self.dialog.iconPathLineEdit.clear()
- self.dialog.licensesTableWidget.setRowCount(0)
- self.dialog.peopleTableWidget.setRowCount(0)
-
- def _predict_author_info(self):
- """ Look at the git commit history and attempt to discern maintainer and author
- information."""
-
- self.git_interface = AddonGitInterface(path)
- if self.git_interface.git_exists:
- committers = self.git_interface.get_last_committers()
- else:
- return
-
- # This is a dictionary keyed to the author's name (which can be many different
- # things, depending on the author) containing two fields, "email" and "count". It
- # is common for there to be multiple entries representing the same human being,
- # so a passing attempt is made to reconcile:
- for key in committers:
- emails = committers[key]["email"]
- if "GitHub" in key:
- # Robotic merge commit (or other similar), ignore
- continue
- # Does any other committer share any of these emails?
- for other_key in committers:
- if other_key == key:
- continue
- other_emails = committers[other_key]["email"]
- for other_email in other_emails:
- if other_email in emails:
- # There is overlap in the two email lists, so this is probably the
- # same author, with a different name (username, pseudonym, etc.)
- if not committers[key]["aka"]:
- committers[key]["aka"] = set()
- committers[key]["aka"].add(other_key)
- committers[key]["count"] += committers[other_key]["count"]
- committers[key]["email"].combine(committers[other_key]["email"])
- committers.remove(other_key)
- break
- maintainers = []
- for name,info in committers.items():
- if info["aka"]:
- for other_name in info["aka"]:
- # Heuristic: the longer name is more likely to be the actual legal name
- if len(other_name) > len(name):
- name = other_name
- # There is no logical basis to choose one email address over another, so just
- # take the first one
- email = info["email"][0]
- commit_count = info["count"]
- maintainers.append( {"name":name,"email":email,"count":commit_count} )
-
- # Sort by count of commits
- maintainers.sort(lambda i:i["count"],reverse=True)
def _setup_dialog_signals(self):
"""Set up the signal and slot connections for the main dialog."""
@@ -442,75 +342,71 @@ class DeveloperMode:
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.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.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.removeContentItemToolButton.clicked.connect(
+ self._remove_content_clicked
+ )
+ self.dialog.contentsListWidget.itemSelectionChanged.connect(
+ self._content_selection_changed
+ )
self.dialog.contentsListWidget.itemDoubleClicked.connect(self._edit_content)
+ self.dialog.versionToTodayButton.clicked.connect(self._set_to_today_clicked)
+
# Finally, populate the combo boxes, etc.
self._populate_combo()
- if self.dialog.pathToAddonComboBox.currentIndex() != -1:
- self._populate_dialog(self.dialog.pathToAddonComboBox.currentText())
# Disable all of the "Remove" buttons until something is selected
- self.dialog.removeLicenseToolButton.setDisabled(True)
- self.dialog.removePersonToolButton.setDisabled(True)
self.dialog.removeContentItemToolButton.setDisabled(True)
def _sync_metadata_to_ui(self):
- """ Take the data from the UI fields and put it into the stored metadata
- object. Only overwrites known data fields: unknown metadata will be retained. """
+ """Take the data from the UI fields and put it into the stored metadata
+ object. Only overwrites known data fields: unknown metadata will be retained."""
+
+ if not self.metadata:
+ self.metadata = FreeCAD.Metadata()
+
self.metadata.Name = self.dialog.displayNameLineEdit.text()
- self.metadata.Description = self.descriptionTextEdit.text()
+ self.metadata.Description = (
+ self.dialog.descriptionTextEdit.document().toPlainText()
+ )
self.metadata.Version = self.dialog.versionLineEdit.text()
self.metadata.Icon = self.dialog.iconPathLineEdit.text()
-
- url = {}
- url["website"] = self.dialog.websiteURLLineEdit.text()
- url["repository"] = self.dialog.repositoryURLLineEdit.text()
- url["bugtracker"] = self.dialog.bugtrackerURLLineEdit.text()
- url["readme"] = self.dialog.readmeURLLineEdit.text()
- url["documentation"] = self.dialog.documentationURLLineEdit.text()
- self.metadata.setUrl(url)
- # Licenses:
- licenses = []
- for row in range(self.dialog.licensesTableWidget.rowCount()):
- license = {}
- license["name"] = self.dialog.licensesTableWidget.item(row,0).text()
- license["file"] = self.dialog.licensesTableWidget.item(row,1).text()
- licenses.append(license)
- self.metadata.setLicense(licenses)
+ urls = []
+ if self.dialog.websiteURLLineEdit.text():
+ urls.append(
+ {"location": self.dialog.websiteURLLineEdit.text(), "type": "website"}
+ )
+ if self.dialog.repositoryURLLineEdit.text():
+ urls.append(
+ {
+ "location": self.dialog.repositoryURLLineEdit.text(),
+ "type": "repository",
+ "branch": self.dialog.branchComboBox.currentText(),
+ }
+ )
+ if self.dialog.bugtrackerURLLineEdit.text():
+ urls.append(
+ {
+ "location": self.dialog.bugtrackerURLLineEdit.text(),
+ "type": "bugtracker",
+ }
+ )
+ if self.dialog.readmeURLLineEdit.text():
+ urls.append(
+ {"location": self.dialog.readmeURLLineEdit.text(), "type": "readme"}
+ )
+ if self.dialog.documentationURLLineEdit.text():
+ urls.append(
+ {
+ "location": self.dialog.documentationURLLineEdit.text(),
+ "type": "documentation",
+ }
+ )
+ self.metadata.Urls = urls
- # Maintainers:
- maintainers = []
- authors = []
- for row in range(self.dialog.peopleTableWidget.rowCount()):
- person = {}
- person["name"] = self.dialog.peopleTableWidget.item(row,1).text()
- person["email"] = self.dialog.peopleTableWidget.item(row,2).text()
- if self.dialog.peopleTableWidget.item(row,0).data(Qt.UserRole) == "maintainer":
- maintainers.append(person)
- elif self.dialog.peopleTableWidget.item(row,0).data(Qt.UserRole) == "author":
- authors.append(person)
-
- # Content:
+ # Content, people, and licenses should already be sync'ed
###############################################################################################
# DIALOG SLOTS
@@ -540,6 +436,8 @@ class DeveloperMode:
if new_text == self.current_mod:
# It doesn't look like it actually changed, bail out
return
+ self.metadata = None
+ self._clear_all_fields()
if not os.path.exists(new_text):
# This isn't a thing (Yet. Maybe the user is still typing?)
return
@@ -588,108 +486,34 @@ class DeveloperMode:
entry_name = f"Mod{i}"
recent_mods_group.SetString(entry_name, mod)
- def _person_selection_changed(self):
- """ Callback: the current selection in the peopleTableWidget changed """
- items = self.dialog.peopleTableWidget.selectedItems()
- if items:
- self.dialog.removePersonToolButton.setDisabled(False)
- else:
- self.dialog.removePersonToolButton.setDisabled(True)
-
- def _license_selection_changed(self):
- """ Callback: the current selection in the licensesTableWidget changed """
- items = self.dialog.licensesTableWidget.selectedItems()
- if items:
- self.dialog.removeLicenseToolButton.setDisabled(False)
- else:
- self.dialog.removeLicenseToolButton.setDisabled(True)
-
- def _add_license_clicked(self):
- """ Callback: The Add License button was clicked """
- license_selector = LicenseSelector(self.current_mod)
- short_code, path = license_selector.exec()
- if short_code:
- self._add_license_row(
- self.dialog.licensesTableWidget.rowCount(), short_code, path
- )
-
- def _remove_license_clicked(self):
- """ Callback: the Remove License button was clicked """
- items = self.dialog.licensesTableWidget.selectedIndexes()
- if items:
- # We only support single-selection, so can just pull the row # from
- # the first entry
- self.dialog.licensesTableWidget.removeRow(items[0].row())
-
- def _edit_license(self, item):
- """ Callback: a license row was double-clicked """
- 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):
- """ Callback: the Add Person button was clicked """
- 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):
- """ Callback: the Remove Person button was clicked """
- items = self.dialog.peopleTableWidget.selectedIndexes()
- if items:
- # We only support single-selection, so can just pull the row # from
- # the first entry
- self.dialog.peopleTableWidget.removeRow(items[0].row())
-
- def _edit_person(self, item):
- """ Callback: a row in the peopleTableWidget was double-clicked """
- 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)
-
-
def _add_content_clicked(self):
- """ Callback: The Add Content button was clicked """
+ """Callback: The Add Content button was clicked"""
dlg = AddContent(self.current_mod, self.metadata)
singleton = False
if self.dialog.contentsListWidget.count() == 0:
singleton = True
- content_type,new_metadata = dlg.exec(singleton=singleton)
+ content_type, new_metadata = dlg.exec(singleton=singleton)
if content_type and new_metadata:
self.metadata.addContentItem(content_type, new_metadata)
self._populate_contents_from_metadata(self.metadata)
def _remove_content_clicked(self):
- """ Callback: the remove content button was clicked """
-
+ """Callback: the remove content button was clicked"""
+
item = self.dialog.contentsListWidget.currentItem()
if not item:
return
content_type = item.data(ContentTypeRole)
content_index = item.data(ContentIndexRole)
- if self.metadata.Content[content_type] and content_index < len(self.metadata.Content[content_type]):
+ if self.metadata.Content[content_type] and content_index < len(
+ self.metadata.Content[content_type]
+ ):
content_name = self.metadata.Content[content_type][content_index].Name
- self.metadata.removeContentItem(content_type,content_name)
+ self.metadata.removeContentItem(content_type, content_name)
self._populate_contents_from_metadata(self.metadata)
def _content_selection_changed(self):
- """ Callback: the selected content item changed """
+ """Callback: the selected content item changed"""
items = self.dialog.contentsListWidget.selectedItems()
if items:
self.dialog.removeContentItemToolButton.setDisabled(False)
@@ -697,7 +521,7 @@ class DeveloperMode:
self.dialog.removeContentItemToolButton.setDisabled(True)
def _edit_content(self, item):
- """ Callback: a content row was double-clicked """
+ """Callback: a content row was double-clicked"""
dlg = AddContent(self.current_mod, self.metadata)
content_type = item.data(ContentTypeRole)
@@ -712,4 +536,10 @@ class DeveloperMode:
self.metadata.addContentItem(new_type, new_metadata)
self._populate_contents_from_metadata(self.metadata)
-
+ def _set_to_today_clicked(self):
+ """Callback: the "set to today" button was clicked"""
+ year = datetime.date.today().year
+ month = datetime.date.today().month
+ day = datetime.date.today().day
+ version_string = f"{year}.{month:>02}.{day:>02}"
+ self.dialog.versionLineEdit.setText(version_string)
diff --git a/src/Mod/AddonManager/addonmanager_devmode_add_content.py b/src/Mod/AddonManager/addonmanager_devmode_add_content.py
index 7a3386547b..e05e0d1373 100644
--- a/src/Mod/AddonManager/addonmanager_devmode_add_content.py
+++ b/src/Mod/AddonManager/addonmanager_devmode_add_content.py
@@ -31,11 +31,31 @@ import FreeCADGui
from Addon import INTERNAL_WORKBENCHES
-from PySide2.QtWidgets import QDialog, QLayout, QFileDialog, QTableWidgetItem
+from PySide2.QtWidgets import (
+ QDialog,
+ QLayout,
+ QFileDialog,
+ QTableWidgetItem,
+ QSizePolicy,
+)
from PySide2.QtGui import QIcon
from PySide2.QtCore import Qt
-from addonmanager_devmode_validators import VersionValidator, NameValidator, PythonIdentifierValidator
+from addonmanager_devmode_license_selector import LicenseSelector
+from addonmanager_devmode_person_editor import PersonEditor
+from addonmanager_devmode_validators import (
+ VersionValidator,
+ NameValidator,
+ PythonIdentifierValidator,
+)
+from addonmanager_devmode_utilities import (
+ populate_people_from_metadata,
+ populate_licenses_from_metadata,
+ add_license_row,
+ add_person_row,
+)
+from addonmanager_devmode_people_table import PeopleTable
+from addonmanager_devmode_licenses_table import LicensesTable
# pylint: disable=too-few-public-methods
@@ -59,6 +79,21 @@ class AddContent:
self.dialog.addonKindComboBox.setItemData(1, "preferencepack")
self.dialog.addonKindComboBox.setItemData(2, "workbench")
+ self.people_table = PeopleTable()
+ self.licenses_table = LicensesTable()
+ large_size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ large_size_policy.setHorizontalStretch(2)
+ small_size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ small_size_policy.setHorizontalStretch(1)
+ self.people_table.widget.setSizePolicy(large_size_policy)
+ self.licenses_table.widget.setSizePolicy(small_size_policy)
+ self.dialog.peopleAndLicenseshorizontalLayout.addWidget(
+ self.people_table.widget
+ )
+ self.dialog.peopleAndLicenseshorizontalLayout.addWidget(
+ self.licenses_table.widget
+ )
+
self.toplevel_metadata = toplevel_metadata
self.metadata = None
self.path_to_addon = path_to_addon.replace("/", os.path.sep)
@@ -83,19 +118,6 @@ class AddContent:
self.dialog.prefPackNameLineEdit.setValidator(NameValidator())
self.dialog.displayNameLineEdit.setValidator(NameValidator())
self.dialog.workbenchClassnameLineEdit.setValidator(PythonIdentifierValidator())
-
- self.dialog.addPersonToolButton.setIcon(
- QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
- )
- self.dialog.removePersonToolButton.setIcon(
- QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
- )
- self.dialog.addLicenseToolButton.setIcon(
- QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
- )
- self.dialog.removeLicenseToolButton.setIcon(
- QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
- )
def exec(
self,
@@ -108,7 +130,9 @@ class AddContent:
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."""
if metadata:
- self.metadata = FreeCAD.Metadata(metadata) # Deep copy
+ self.metadata = FreeCAD.Metadata(metadata) # Deep copy
+ else:
+ self.metadata = FreeCAD.Metadata()
self.dialog.singletonCheckBox.setChecked(singleton)
if singleton:
# This doesn't happen automatically the first time
@@ -125,9 +149,6 @@ class AddContent:
self.dialog.addonKindComboBox.setCurrentIndex(index)
if metadata:
self._populate_dialog(metadata)
-
- self.dialog.removeLicenseToolButton.setDisabled(True)
- self.dialog.removePersonToolButton.setDisabled(True)
self.dialog.layout().setSizeConstraint(QLayout.SetFixedSize)
result = self.dialog.exec()
@@ -170,7 +191,8 @@ class AddContent:
self.dialog.descriptionTextEdit.setPlainText(metadata.Description)
self.dialog.versionLineEdit.setText(metadata.Version)
- #TODO: Add people and licenses
+ self.people_table.show(metadata)
+ self.licenses_table.show(metadata, self.path_to_addon)
def _set_icon(self, icon_relative_path):
"""Load the icon and display it, and its path, in the dialog."""
@@ -199,7 +221,7 @@ class AddContent:
##########################################################################################
# Required data:
- current_data:str = self.dialog.addonKindComboBox.currentData()
+ current_data: str = self.dialog.addonKindComboBox.currentData()
if current_data == "preferencepack":
self.metadata.Name = self.dialog.prefPackNameLineEdit.text()
elif self.dialog.displayNameLineEdit.text():
@@ -208,17 +230,20 @@ class AddContent:
if current_data == "workbench":
self.metadata.Classname = self.dialog.workbenchClassnameLineEdit.text()
elif current_data == "macro":
- self.metadata.File = [self.dialog.macroFileLineEdit.text()]
+ self.metadata.File = [self.dialog.macroFileLineEdit.text()]
##########################################################################################
+ self.metadata.Subdirectory = self.dialog.subdirectoryLineEdit.text()
+ self.metadata.Icon = self.dialog.iconLineEdit.text()
+
# Early return if this is the only addon
if self.dialog.singletonCheckBox.isChecked():
return (current_data, self.metadata)
- self.metadata.Icon = self.dialog.iconLineEdit.text()
-
# Otherwise, process the rest of the metadata (display name is already done)
- self.metadata.Description = self.dialog.descriptionTextEdit.document().toPlainText()
+ self.metadata.Description = (
+ self.dialog.descriptionTextEdit.document().toPlainText()
+ )
self.metadata.Version = self.dialog.versionLineEdit.text()
maintainers = []
@@ -228,9 +253,9 @@ class AddContent:
name = self.dialog.peopleTableWidget.item(row, 1).text()
email = self.dialog.peopleTableWidget.item(row, 2).text()
if person_type == "maintainer":
- maintainers.append({"name":name,"email":email})
+ maintainers.append({"name": name, "email": email})
elif person_type == "author":
- authors.append({"name":name,"email":email})
+ authors.append({"name": name, "email": email})
self.metadata.Maintainer = maintainers
self.metadata.Author = authors
@@ -241,7 +266,7 @@ class AddContent:
license["file"] = self.dialog.licensesTableWidget.item(row, 1).text()
licenses.append(license)
self.metadata.License = licenses
-
+
return (self.dialog.addonKindComboBox.currentData(), self.metadata)
###############################################################################################
@@ -347,6 +372,97 @@ class AddContent:
dlg = EditDependencies()
result = dlg.exec(self.metadata)
+ def _person_selection_changed(self):
+ """Callback: the current selection in the peopleTableWidget changed"""
+ items = self.dialog.peopleTableWidget.selectedItems()
+ if items:
+ self.dialog.removePersonToolButton.setDisabled(False)
+ else:
+ self.dialog.removePersonToolButton.setDisabled(True)
+
+ def _license_selection_changed(self):
+ """Callback: the current selection in the licensesTableWidget changed"""
+ items = self.dialog.licensesTableWidget.selectedItems()
+ if items:
+ self.dialog.removeLicenseToolButton.setDisabled(False)
+ else:
+ self.dialog.removeLicenseToolButton.setDisabled(True)
+
+ def _add_license_clicked(self):
+ """Callback: The Add License button was clicked"""
+ license_selector = LicenseSelector(self.current_mod)
+ short_code, path = license_selector.exec()
+ if short_code:
+ add_license_row(
+ self.dialog.licensesTableWidget.rowCount(),
+ short_code,
+ path,
+ self.path_to_addon,
+ self.dialog.licensesTableWidget,
+ )
+
+ def _remove_license_clicked(self):
+ """Callback: the Remove License button was clicked"""
+ items = self.dialog.licensesTableWidget.selectedIndexes()
+ if items:
+ # We only support single-selection, so can just pull the row # from
+ # the first entry
+ self.dialog.licensesTableWidget.removeRow(items[0].row())
+
+ def _edit_license(self, item):
+ """Callback: a license row was double-clicked"""
+ 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)
+ add_license_row(
+ row,
+ short_code,
+ path,
+ self.path_to_addon,
+ self.dialog.licensesTableWidget,
+ )
+
+ def _add_person_clicked(self):
+ """Callback: the Add Person button was clicked"""
+ dlg = PersonEditor()
+ person_type, name, email = dlg.exec()
+ if person_type and name:
+ add_person_row(
+ self.dialog.peopleTableWidget.rowCount(),
+ person_type,
+ name,
+ email,
+ self.dialog.peopleTableWidget,
+ )
+
+ def _remove_person_clicked(self):
+ """Callback: the Remove Person button was clicked"""
+ items = self.dialog.peopleTableWidget.selectedIndexes()
+ if items:
+ # We only support single-selection, so can just pull the row # from
+ # the first entry
+ self.dialog.peopleTableWidget.removeRow(items[0].row())
+
+ def _edit_person(self, item):
+ """Callback: a row in the peopleTableWidget was double-clicked"""
+ 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)
+ add_person_row(row, person_type, name, email, self.dialog.peopleTableWidget)
+ self.dialog.peopleTableWidget.selectRow(row)
+
class EditTags:
"""A dialog to edit tags"""
@@ -398,9 +514,9 @@ class EditDependencies:
self.dialog.removeDependencyToolButton.setDisabled(True)
- def exec(self, metadata:FreeCAD.Metadata):
+ def exec(self, metadata: FreeCAD.Metadata):
"""Execute the dialog"""
- self.metadata = FreeCAD.Metadata(metadata) # Make a copy, in case we cancel
+ self.metadata = FreeCAD.Metadata(metadata) # Make a copy, in case we cancel
row = 0
for dep in self.metadata.Depend:
dep_type = dep["type"]
@@ -419,8 +535,9 @@ class EditDependencies:
if dep_name:
row = self.dialog.tableWidget.rowCount()
self._add_row(row, dep_type, dep_name, dep_optional)
- self.metadata.addDepend({"package":dep_name, "type":dep_type, "optional":dep_optional})
-
+ self.metadata.addDepend(
+ {"package": dep_name, "type": dep_type, "optional": dep_optional}
+ )
def _add_row(self, row, dep_type, dep_name, dep_optional):
"""Utility function to add a row to the table."""
@@ -449,7 +566,9 @@ class EditDependencies:
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))
- self.metadata.removeDepend({"package":dep_name, "type":dep_type, "optional":dep_optional})
+ self.metadata.removeDepend(
+ {"package": dep_name, "type": dep_type, "optional": dep_optional}
+ )
self.dialog.tableWidget.removeRow(row)
def _edit_dependency(self, item):
@@ -459,10 +578,20 @@ class EditDependencies:
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))
- new_dep_type, new_dep_name, new_dep_optional = dlg.exec(dep_type, dep_name, dep_optional)
+ new_dep_type, new_dep_name, new_dep_optional = dlg.exec(
+ dep_type, dep_name, dep_optional
+ )
if dep_type and dep_name:
- self.metadata.removeDepend({"package":dep_name, "type":dep_type, "optional":dep_optional})
- self.metadata.addDepend({"package":new_dep_name, "type":new_dep_type, "optional":new_dep_optional})
+ self.metadata.removeDepend(
+ {"package": dep_name, "type": dep_type, "optional": dep_optional}
+ )
+ self.metadata.addDepend(
+ {
+ "package": new_dep_name,
+ "type": new_dep_type,
+ "optional": new_dep_optional,
+ }
+ )
self.dialog.tableWidget.removeRow(row)
self._add_row(row, dep_type, dep_name, dep_optional)
@@ -499,7 +628,7 @@ class EditDependency:
)
# Expect mostly Python dependencies...
- self.dialog.typeComboBox.setCurrentIndex(2)
+ self.dialog.typeComboBox.setCurrentIndex(2)
def exec(
self, dep_type="", dep_name="", dep_optional=False
@@ -541,7 +670,7 @@ class EditDependency:
def _populate_external_addons(self):
"""Add all known addons to the list"""
self.dialog.dependencyComboBox.clear()
- #pylint: disable=import-outside-toplevel
+ # pylint: disable=import-outside-toplevel
from AddonManager import INSTANCE as AM_INSTANCE
repo_dict = {}
@@ -561,7 +690,7 @@ class EditDependency:
def _populate_allowed_python_packages(self):
"""Add all allowed python packages to the list"""
self.dialog.dependencyComboBox.clear()
- #pylint: disable=import-outside-toplevel
+ # pylint: disable=import-outside-toplevel
from AddonManager import INSTANCE as AM_INSTANCE
packages = sorted(AM_INSTANCE.allowed_packages)
@@ -601,7 +730,7 @@ class EditFreeCADVersions:
)
)
- def exec(self, metadata:FreeCAD.Metadata):
+ def exec(self, metadata: FreeCAD.Metadata):
"""Execute the dialog"""
if metadata.FreeCADMin != "0.0.0":
self.dialog.minVersionLineEdit.setText(metadata.FreeCADMin)
diff --git a/src/Mod/AddonManager/addonmanager_devmode_dependencies.py b/src/Mod/AddonManager/addonmanager_devmode_dependencies.py
index 7a1d4979ae..2c7df548b0 100644
--- a/src/Mod/AddonManager/addonmanager_devmode_dependencies.py
+++ b/src/Mod/AddonManager/addonmanager_devmode_dependencies.py
@@ -19,4 +19,3 @@
# * . *
# * *
# ***************************************************************************
-
diff --git a/src/Mod/AddonManager/addonmanager_devmode_license_selector.py b/src/Mod/AddonManager/addonmanager_devmode_license_selector.py
index ca2e2c1df6..eff0a28055 100644
--- a/src/Mod/AddonManager/addonmanager_devmode_license_selector.py
+++ b/src/Mod/AddonManager/addonmanager_devmode_license_selector.py
@@ -24,7 +24,7 @@
import os
from datetime import date
-from typing import Optional
+from typing import Optional, Tuple
import FreeCAD
import FreeCADGui
@@ -114,7 +114,9 @@ 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 = "") -> Optional[str]:
+ def exec(
+ self, short_code: str = None, license_path: str = ""
+ ) -> Optional[Tuple[str, 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."""
diff --git a/src/Mod/AddonManager/addonmanager_devmode_licenses_table.py b/src/Mod/AddonManager/addonmanager_devmode_licenses_table.py
new file mode 100644
index 0000000000..e10d14f477
--- /dev/null
+++ b/src/Mod/AddonManager/addonmanager_devmode_licenses_table.py
@@ -0,0 +1,127 @@
+# ***************************************************************************
+# * *
+# * 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 wrapper class for a table listing authors and maintainers """
+
+import os
+
+from PySide2.QtWidgets import QTableWidgetItem
+from PySide2.QtGui import QIcon
+from PySide2.QtCore import Qt
+
+import FreeCAD
+import FreeCADGui
+
+from addonmanager_devmode_license_selector import LicenseSelector
+
+translate = FreeCAD.Qt.translate
+
+
+class LicensesTable:
+ """A QTableWidget and associated buttons for managing the list of authors and maintainers."""
+
+ def __init__(self):
+ self.widget = FreeCADGui.PySideUic.loadUi(
+ os.path.join(os.path.dirname(__file__), "developer_mode_licenses_table.ui")
+ )
+
+ self.widget.addButton.setIcon(
+ QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
+ )
+ self.widget.removeButton.setIcon(
+ QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
+ )
+
+ self.widget.addButton.clicked.connect(self._add_clicked)
+ self.widget.removeButton.clicked.connect(self._remove_clicked)
+ self.widget.tableWidget.itemSelectionChanged.connect(self._selection_changed)
+ self.widget.tableWidget.itemDoubleClicked.connect(self._edit)
+
+ def show(self, metadata, path_to_addon):
+ """Set up the widget based on incoming metadata"""
+ self.metadata = metadata
+ self.path_to_addon = path_to_addon
+ self._populate_from_metadata()
+ self.widget.removeButton.setDisabled(True)
+ self.widget.show()
+
+ def _populate_from_metadata(self):
+ """Use the passed metadata object to populate the maintainers and authors"""
+ self.widget.tableWidget.setRowCount(0)
+ row = 0
+ for license in self.metadata.License:
+ shortcode = license["name"]
+ path = license["file"]
+ self._add_row(row, shortcode, path)
+ row += 1
+
+ def _add_row(self, row, shortcode, path):
+ """Add this license to the tableWidget at row given"""
+ self.widget.tableWidget.insertRow(row)
+ self.widget.tableWidget.setItem(row, 0, QTableWidgetItem(shortcode))
+ self.widget.tableWidget.setItem(row, 1, QTableWidgetItem(path))
+
+ def _add_clicked(self):
+ """Callback: the Add License button was clicked"""
+ dlg = LicenseSelector(self.path_to_addon)
+ shortcode, path = dlg.exec()
+ if shortcode and path:
+ self._add_row(self.widget.tableWidget.rowCount(), shortcode, path)
+ self.metadata.addLicense(shortcode, path)
+
+ def _remove_clicked(self):
+ """Callback: the Remove License button was clicked"""
+ items = self.widget.tableWidget.selectedIndexes()
+ if items:
+ # We only support single-selection, so can just pull the row # from
+ # the first entry
+ row = items[0].row()
+ shortcode = self.widget.tableWidget.item(row, 0).text()
+ path = self.widget.tableWidget.item(row, 1).text()
+ self.widget.tableWidget.removeRow(row)
+ self.metadata.removeLicense(shortcode, path)
+
+ def _edit(self, item):
+ """Callback: a row in the tableWidget was double-clicked"""
+ row = item.row()
+ shortcode = self.widget.tableWidget.item(row, 0).text()
+ path = self.widget.tableWidget.item(row, 1).text()
+
+ dlg = LicenseSelector(self.path_to_addon)
+ new_shortcode, new_path = dlg.exec(shortcode, path)
+
+ if new_shortcode and new_path:
+ self.widget.tableWidget.removeRow(row)
+ self.metadata.removeLicense(new_shortcode, new_path)
+
+ self._add_row(row, new_shortcode, new_path)
+ self.metadata.addLicense(new_shortcode, new_path)
+
+ self.widget.tableWidget.selectRow(row)
+
+ def _selection_changed(self):
+ """Callback: the current selection in the tableWidget changed"""
+ items = self.widget.tableWidget.selectedItems()
+ if items:
+ self.widget.removeButton.setDisabled(False)
+ else:
+ self.widget.removeButton.setDisabled(True)
diff --git a/src/Mod/AddonManager/addonmanager_devmode_people_table.py b/src/Mod/AddonManager/addonmanager_devmode_people_table.py
new file mode 100644
index 0000000000..08ea9ef4f7
--- /dev/null
+++ b/src/Mod/AddonManager/addonmanager_devmode_people_table.py
@@ -0,0 +1,151 @@
+# ***************************************************************************
+# * *
+# * 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 wrapper class for a table listing authors and maintainers """
+
+import os
+
+from PySide2.QtWidgets import QTableWidgetItem
+from PySide2.QtGui import QIcon
+from PySide2.QtCore import Qt
+
+import FreeCAD
+import FreeCADGui
+
+from addonmanager_devmode_person_editor import PersonEditor
+
+translate = FreeCAD.Qt.translate
+
+
+class PeopleTable:
+ """A QTableWidget and associated buttons for managing the list of authors and maintainers."""
+
+ def __init__(self):
+ self.widget = FreeCADGui.PySideUic.loadUi(
+ os.path.join(os.path.dirname(__file__), "developer_mode_people_table.ui")
+ )
+
+ self.widget.addButton.setIcon(
+ QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
+ )
+ self.widget.removeButton.setIcon(
+ QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
+ )
+
+ self.widget.addButton.clicked.connect(self._add_clicked)
+ self.widget.removeButton.clicked.connect(self._remove_clicked)
+ self.widget.tableWidget.itemSelectionChanged.connect(self._selection_changed)
+ self.widget.tableWidget.itemDoubleClicked.connect(self._edit)
+
+ def show(self, metadata):
+ """Set up the widget based on incoming metadata"""
+ self.metadata = metadata
+ self._populate_from_metadata()
+ self.widget.removeButton.setDisabled(True)
+ self.widget.show()
+
+ def _populate_from_metadata(self):
+ """Use the passed metadata object to populate the maintainers and authors"""
+ self.widget.tableWidget.setRowCount(0)
+ row = 0
+ for maintainer in self.metadata.Maintainer:
+ name = maintainer["name"]
+ email = maintainer["email"]
+ self._add_row(row, "maintainer", name, email)
+ row += 1
+ for author in self.metadata.Author:
+ name = author["name"]
+ email = author["email"]
+ self._add_row(row, "author", name, email)
+ row += 1
+
+ def _add_row(self, row, person_type, name, email):
+ """Add this person to the tableWidget at row given"""
+ person_type_translation = {
+ "maintainer": translate("AddonsInstaller", "Maintainer"),
+ "author": translate("AddonsInstaller", "Author"),
+ }
+ self.widget.tableWidget.insertRow(row)
+ item = QTableWidgetItem(person_type_translation[person_type])
+ item.setData(Qt.UserRole, person_type)
+ self.widget.tableWidget.setItem(row, 0, item)
+ self.widget.tableWidget.setItem(row, 1, QTableWidgetItem(name))
+ self.widget.tableWidget.setItem(row, 2, QTableWidgetItem(email))
+
+ def _add_clicked(self):
+ """Callback: the Add Person button was clicked"""
+ dlg = PersonEditor()
+ person_type, name, email = dlg.exec()
+ if person_type and name:
+ self._add_row(self.widget.tableWidget.rowCount(), person_type, name, email)
+ if person_type == "maintainer":
+ self.metadata.addMaintainer(name, email)
+ else:
+ self.metadata.addAuthor(name, email)
+
+ def _remove_clicked(self):
+ """Callback: the Remove Person button was clicked"""
+ items = self.widget.tableWidget.selectedIndexes()
+ if items:
+ # We only support single-selection, so can just pull the row # from
+ # the first entry
+ row = items[0].row()
+ person_type = self.widget.tableWidget.item(row, 0).data(Qt.UserRole)
+ name = self.widget.tableWidget.item(row, 1).text()
+ email = self.widget.tableWidget.item(row, 2).text()
+ self.widget.tableWidget.removeRow(row)
+ if person_type == "maintainer":
+ self.metadata.removeMaintainer(name, email)
+ else:
+ self.metadata.removeAuthor(name, email)
+
+ def _edit(self, item):
+ """Callback: a row in the tableWidget was double-clicked"""
+ row = item.row()
+ person_type = self.widget.tableWidget.item(row, 0).data(Qt.UserRole)
+ name = self.widget.tableWidget.item(row, 1).text()
+ email = self.widget.tableWidget.item(row, 2).text()
+
+ dlg = PersonEditor()
+ dlg.setup(person_type, name, email)
+ new_person_type, new_name, new_email = dlg.exec()
+
+ if new_person_type and new_name:
+ self.widget.tableWidget.removeRow(row)
+ if person_type == "maintainer":
+ self.metadata.removeMaintainer(name, email)
+ else:
+ self.metadata.removeAuthor(name, email)
+ self._add_row(row, new_person_type, new_name, email)
+ if new_person_type == "maintainer":
+ self.metadata.addMaintainer(new_name, new_email)
+ else:
+ self.metadata.addAuthor(new_name, new_email)
+ self.widget.tableWidget.selectRow(row)
+
+ def _selection_changed(self):
+ """Callback: the current selection in the tableWidget changed"""
+ items = self.widget.tableWidget.selectedItems()
+ if items:
+ self.widget.removeButton.setDisabled(False)
+ else:
+ self.widget.removeButton.setDisabled(True)
diff --git a/src/Mod/AddonManager/addonmanager_devmode_predictor.py b/src/Mod/AddonManager/addonmanager_devmode_predictor.py
index 8a9eb9f810..0c3063f38b 100644
--- a/src/Mod/AddonManager/addonmanager_devmode_predictor.py
+++ b/src/Mod/AddonManager/addonmanager_devmode_predictor.py
@@ -28,23 +28,41 @@ endorsement of any particular license. In addition, the inclusion of those text
does not imply a modification to the license for THIS software, which is licensed
under the LGPLv2.1 license (as stated above)."""
+import datetime
import os
import FreeCAD
-from addonmanager_git import initialize_git, GitFailed
+from addonmanager_git import initialize_git, GitFailed, GitManager
+from addonmanager_utilities import get_readme_url
+
+translate = FreeCAD.Qt.translate
+
+
+class AddonSlice:
+ """A tiny class to implement duck-typing for the URL-parsing utility functions"""
+
+ def __init__(self, url, branch):
+ self.url = url
+ self.branch = branch
+
class Predictor:
- """ Guess the appropriate metadata to apply to a project based on various parameters
- found in the supplied directory. """
+ """Guess the appropriate metadata to apply to a project based on various parameters
+ found in the supplied directory."""
def __init__(self):
self.path = None
self.metadata = FreeCAD.Metadata()
self.license_data = None
self.license_file = ""
+ self.git_manager: GitManager = initialize_git()
+ if not self.git_manager:
+ raise Exception("Cannot use Developer Mode without git installed")
- def predict_metadata(self, path:os.PathLike) -> FreeCAD.Metadata:
- """ Create a predicted Metadata object based on the contents of the passed-in directory """
+ def predict_metadata(self, path: os.PathLike) -> FreeCAD.Metadata:
+ """Create a predicted Metadata object based on the contents of the passed-in directory"""
+ if not os.path.isdir(path):
+ return None
self.path = path
self._predict_author_info()
self._predict_name()
@@ -53,102 +71,207 @@ class Predictor:
self._predict_icon()
self._predict_urls()
self._predict_license()
+ self._predict_version()
+
+ return self.metadata
def _predict_author_info(self):
- """ Predict the author and maintainer info based on git history """
+ """Look at the git commit history and attempt to discern maintainer and author
+ information."""
+
+ committers = self.git_manager.get_last_committers(self.path)
+
+ # This is a dictionary keyed to the author's name (which can be many different
+ # things, depending on the author) containing two fields, "email" and "count". It
+ # is common for there to be multiple entries representing the same human being,
+ # so a passing attempt is made to reconcile:
+ filtered_committers = {}
+ for key in committers:
+ emails = committers[key]["email"]
+ if "github" in key.lower():
+ # Robotic merge commit (or other similar), ignore
+ continue
+ # Does any other committer share any of these emails?
+ for other_key in committers:
+ if other_key == key:
+ continue
+ other_emails = committers[other_key]["email"]
+ for other_email in other_emails:
+ if other_email in emails:
+ # There is overlap in the two email lists, so this is probably the
+ # same author, with a different name (username, pseudonym, etc.)
+ if not committers[key]["aka"]:
+ committers[key]["aka"] = set()
+ committers[key]["aka"].add(other_key)
+ committers[key]["count"] += committers[other_key]["count"]
+ committers[key]["email"].combine(committers[other_key]["email"])
+ committers.remove(other_key)
+ break
+ filtered_committers[key] = committers[key]
+ maintainers = []
+ for name, info in filtered_committers.items():
+ if "aka" in info:
+ for other_name in info["aka"]:
+ # Heuristic: the longer name is more likely to be the actual legal name
+ if len(other_name) > len(name):
+ name = other_name
+ # There is no logical basis to choose one email address over another, so just
+ # take the first one
+ email = info["email"][0]
+ commit_count = info["count"]
+ maintainers.append({"name": name, "email": email, "count": commit_count})
+
+ # Sort by count of commits
+ maintainers.sort(key=lambda i: i["count"], reverse=True)
+
+ self.metadata.Maintainer = maintainers
def _predict_name(self):
- """ Predict the name based on the local path name and/or the contents of a
- README.md file. """
+ """Predict the name based on the local path name and/or the contents of a
+ README.md file."""
+
+ normed_path = self.path.replace(os.path.sep, "/")
+ path_components = normed_path.split("/")
+ final_path_component = path_components[-1]
+ predicted_name = final_path_component.replace("/", "")
+ self.metadata.Name = predicted_name
def _predict_description(self):
- """ Predict the description based on the contents of a README.md file. """
+ """Predict the description based on the contents of a README.md file."""
+ self._load_readme()
+
+ if not self.readme_data:
+ return
+
+ lines = self.readme_data.split("\n")
+ description = ""
+ for line in lines:
+ if "#" in line:
+ continue # Probably not a line of description
+ if "![" in line:
+ continue # An image link, probably separate from any description
+ if not line and description:
+ break # We're done: this is a blank line, and we've read some data already
+ if description:
+ description += " "
+ description += line
+
+ if description:
+ self.metadata.Description = description
def _predict_contents(self):
- """ Predict the contents based on the contents of the directory. """
+ """Predict the contents based on the contents of the directory."""
def _predict_icon(self):
- """ Predict the icon based on either a class which defines an Icon member, or
- the contents of the local directory structure. """
+ """Predict the icon based on either a class which defines an Icon member, or
+ the contents of the local directory structure."""
def _predict_urls(self):
- """ Predict the URLs based on git settings """
+ """Predict the URLs based on git settings"""
+
+ branch = self.git_manager.current_branch(self.path)
+ remote = self.git_manager.get_remote(self.path)
+
+ addon = AddonSlice(remote, branch)
+ readme = get_readme_url(addon)
+
+ self.metadata.addUrl("repository", remote, branch)
+ self.metadata.addUrl("readme", readme)
def _predict_license(self):
- """ Predict the license based on any existing license file. """
+ """Predict the license based on any existing license file."""
- # These are processed in order, so the BSD 3 clause must come before the 2, for example, because
- # the only difference between them is the additional clause.
+ # These are processed in order, so the BSD 3 clause must come before the 2, for example, because
+ # the only difference between them is the additional clause.
known_strings = {
- "Apache-2.0": (
- "Apache License, Version 2.0",
- "Apache License\nVersion 2.0, January 2004",
- ),
- "BSD-3-Clause": (
- "The 3-Clause BSD License",
- "3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission."
- ),
- "BSD-2-Clause": (
- "The 2-Clause BSD License",
- "2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution."
- ),
- "CC0v1": (
- "CC0 1.0 Universal",
- "voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms"
- ),
- "GPLv2": (
- "GNU General Public License version 2",
- "GNU GENERAL PUBLIC LICENSE\nVersion 2, June 1991"
- ),
- "GPLv3": (
- "GNU General Public License version 3",
- "The GNU General Public License is a free, copyleft license for software and other kinds of works."
- ),
- "LGPLv2.1": (
- "GNU Lesser General Public License version 2.1",
- "GNU Lesser General Public License\nVersion 2.1, February 1999"
- ),
- "LGPLv3": (
- "GNU Lesser General Public License version 3",
- "GNU LESSER GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007"
- ),
- "MIT": (
- "The MIT License",
- "including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software",
- ),
- "MPL-2.0": (
- "Mozilla Public License 2.0",
- "https://opensource.org/licenses/MPL-2.0",
- ),
- }
+ "Apache-2.0": (
+ "Apache License, Version 2.0",
+ "Apache License\nVersion 2.0, January 2004",
+ ),
+ "BSD-3-Clause": (
+ "The 3-Clause BSD License",
+ "3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.",
+ ),
+ "BSD-2-Clause": (
+ "The 2-Clause BSD License",
+ "2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.",
+ ),
+ "CC0v1": (
+ "CC0 1.0 Universal",
+ "voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms",
+ ),
+ "GPLv2": (
+ "GNU General Public License version 2",
+ "GNU GENERAL PUBLIC LICENSE\nVersion 2, June 1991",
+ ),
+ "GPLv3": (
+ "GNU General Public License version 3",
+ "The GNU General Public License is a free, copyleft license for software and other kinds of works.",
+ ),
+ "LGPLv2.1": (
+ "GNU Lesser General Public License version 2.1",
+ "GNU Lesser General Public License\nVersion 2.1, February 1999",
+ ),
+ "LGPLv3": (
+ "GNU Lesser General Public License version 3",
+ "GNU LESSER GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007",
+ ),
+ "MIT": (
+ "The MIT License",
+ "including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software",
+ ),
+ "MPL-2.0": (
+ "Mozilla Public License 2.0",
+ "https://opensource.org/licenses/MPL-2.0",
+ ),
+ }
self._load_license()
if self.license_data:
for license, test_data in known_strings.items():
- if license in self.license_data:
- self.metadata.License = {"name":license,"file":self.license_file}
+ if license.lower() in self.license_data.lower():
+ self.metadata.addLicense(license, self.license_file)
return
for test_text in test_data:
- if test_text in self.license_data
- self.metadata.License = {"name":license,"file":self.license_file}
+ # Do the comparison without regard to whitespace or capitalization
+ if (
+ "".join(test_text.split()).lower()
+ in "".join(self.license_data.split()).lower()
+ ):
+ self.metadata.addLicense(license, self.license_file)
return
+ def _predict_version(self):
+ """Default to a CalVer style set to today's date"""
+ year = datetime.date.today().year
+ month = datetime.date.today().month
+ day = datetime.date.today().day
+ version_string = f"{year}.{month:>02}.{day:>02}"
+ self.metadata.Version = version_string
+
def _load_readme(self):
- """ Load in any existing readme """
+ """Load in any existing readme"""
valid_names = ["README.md", "README.txt", "README"]
for name in valid_names:
- full_path = os.path.join(self.path,name)
+ full_path = os.path.join(self.path, name)
if os.path.exists(full_path):
- with open(full_path,"r",encoding="utf-8") as f:
+ with open(full_path, "r", encoding="utf-8") as f:
self.readme_data = f.read()
return
def _load_license(self):
- """ Load in any existing license """
- valid_names = ["LICENSE", "LICENCE", "COPYING","LICENSE.txt", "LICENCE.txt", "COPYING.txt"]
+ """Load in any existing license"""
+ valid_names = [
+ "LICENSE",
+ "LICENCE",
+ "COPYING",
+ "LICENSE.txt",
+ "LICENCE.txt",
+ "COPYING.txt",
+ ]
for name in valid_names:
- full_path = os.path.join(self.path,name)
- if os.path.exists(full_path):
- with open(full_path,"r",encoding="utf-8") as f:
+ full_path = os.path.join(self.path.replace("/", os.path.sep), name)
+ if os.path.isfile(full_path):
+ with open(full_path, "r", encoding="utf-8") as f:
self.license_data = f.read()
self.license_file = name
- return
\ No newline at end of file
+ return
diff --git a/src/Mod/AddonManager/addonmanager_devmode_validators.py b/src/Mod/AddonManager/addonmanager_devmode_validators.py
index 7aacb46dc9..aefb5e8652 100644
--- a/src/Mod/AddonManager/addonmanager_devmode_validators.py
+++ b/src/Mod/AddonManager/addonmanager_devmode_validators.py
@@ -30,6 +30,7 @@ from PySide2.QtGui import (
)
from PySide2.QtCore import QRegularExpression
+
def isidentifier(ident: str) -> bool:
if not ident.isidentifier():
@@ -40,6 +41,7 @@ def isidentifier(ident: str) -> bool:
return True
+
class NameValidator(QValidator):
"""Simple validator to exclude characters that are not valid in filenames."""
@@ -62,18 +64,20 @@ class NameValidator(QValidator):
class PythonIdentifierValidator(QValidator):
- """ Validates whether input is a valid Python identifier. """
+ """Validates whether input is a valid Python identifier."""
def validate(self, value: str, _: int):
if not value:
return QValidator.Intermediate
if not value.isidentifier():
- return QValidator.Invalid # Includes an illegal character of some sort
+ return QValidator.Invalid # Includes an illegal character of some sort
if keyword.iskeyword(value):
- return QValidator.Intermediate # They can keep typing and it might become valid
-
+ return (
+ QValidator.Intermediate
+ ) # They can keep typing and it might become valid
+
return QValidator.Acceptable
@@ -142,4 +146,4 @@ class VersionValidator(QValidator):
return semver_result
if calver_result[0] == QValidator.Intermediate:
return calver_result
- return (QValidator.Invalid, value, position)
\ No newline at end of file
+ return (QValidator.Invalid, value, position)
diff --git a/src/Mod/AddonManager/addonmanager_git.py b/src/Mod/AddonManager/addonmanager_git.py
index c5558633a7..a3703927a8 100644
--- a/src/Mod/AddonManager/addonmanager_git.py
+++ b/src/Mod/AddonManager/addonmanager_git.py
@@ -120,7 +120,9 @@ class GitManager:
+ "...\n"
)
remote = self.get_remote(local_path)
- with open(os.path.join(local_path, "ADDON_DISABLED"), "w", encoding="utf-8") as f:
+ with open(
+ os.path.join(local_path, "ADDON_DISABLED"), "w", encoding="utf-8"
+ ) as f:
f.write(
"This is a backup of an addon that failed to update cleanly so was re-cloned. "
+ "It was disabled by the Addon Manager's git update facility and can be "
@@ -195,7 +197,9 @@ class GitManager:
# branch = self._synchronous_call_git(["branch", "--show-current"]).strip()
# This is more universal (albeit more opaque to the reader):
- branch = self._synchronous_call_git(["rev-parse", "--abbrev-ref", "HEAD"]).strip()
+ branch = self._synchronous_call_git(
+ ["rev-parse", "--abbrev-ref", "HEAD"]
+ ).strip()
except GitFailed as e:
os.chdir(old_dir)
raise e
@@ -257,11 +261,13 @@ class GitManager:
return result
def get_branches(self, local_path) -> List[str]:
- """ Get a list of all available branches (local and remote) """
+ """Get a list of all available branches (local and remote)"""
old_dir = os.getcwd()
os.chdir(local_path)
try:
- stdout = self._synchronous_call_git(["branch", "-a", "--format=%(refname:lstrip=2)"])
+ stdout = self._synchronous_call_git(
+ ["branch", "-a", "--format=%(refname:lstrip=2)"]
+ )
except GitFailed as e:
os.chdir(old_dir)
raise e
@@ -272,17 +278,24 @@ class GitManager:
return branches
def get_last_committers(self, local_path, n=10):
- """ Examine the last n entries of the commit history, and return a list of all of the
- committers, their email addresses, and how many commits each one is responsible for. """
+ """Examine the last n entries of the commit history, and return a list of all of the
+ committers, their email addresses, and how many commits each one is responsible for."""
old_dir = os.getcwd()
os.chdir(local_path)
- authors = self._synchronous_call_git(["log", f"-{n}", "--format=%cN"])
- emails = self._synchronous_call_git(["log", f"-{n}", "--format=%cE"])
+ authors = self._synchronous_call_git(["log", f"-{n}", "--format=%cN"]).split(
+ "\n"
+ )
+ emails = self._synchronous_call_git(["log", f"-{n}", "--format=%cE"]).split(
+ "\n"
+ )
os.chdir(old_dir)
result_dict = {}
- for author,email in zip(authors,emails):
+ for author, email in zip(authors, emails):
+ if not author or not email:
+ continue
if author not in result_dict:
+ result_dict[author] = {}
result_dict[author]["email"] = [email]
result_dict[author]["count"] = 1
else:
@@ -294,8 +307,8 @@ class GitManager:
return result_dict
def get_last_authors(self, local_path, n=10):
- """ Examine the last n entries of the commit history, and return a list of all of the
- authors, their email addresses, and how many commits each one is responsible for. """
+ """Examine the last n entries of the commit history, and return a list of all of the
+ authors, their email addresses, and how many commits each one is responsible for."""
old_dir = os.getcwd()
os.chdir(local_path)
authors = self._synchronous_call_git(["log", f"-{n}", "--format=%aN"])
@@ -303,7 +316,7 @@ class GitManager:
os.chdir(old_dir)
result_dict = {}
- for author,email in zip(authors,emails):
+ for author, email in zip(authors, emails):
if author not in result_dict:
result_dict[author]["email"] = [email]
result_dict[author]["count"] = 1
@@ -348,7 +361,7 @@ class GitManager:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
- shell=True # On Windows this will prevent all the pop-up consoles
+ shell=True, # On Windows this will prevent all the pop-up consoles
)
else:
proc = subprocess.run(
diff --git a/src/Mod/AddonManager/addonmanager_workers_utility.py b/src/Mod/AddonManager/addonmanager_workers_utility.py
index 1f612ab842..dd04e9057a 100644
--- a/src/Mod/AddonManager/addonmanager_workers_utility.py
+++ b/src/Mod/AddonManager/addonmanager_workers_utility.py
@@ -27,6 +27,8 @@ import FreeCAD
from PySide2 import QtCore
import NetworkManager
+translate = FreeCAD.Qt.translate
+
class ConnectionChecker(QtCore.QThread):
"""A worker thread for checking the connection to GitHub as a proxy for overall
diff --git a/src/Mod/AddonManager/developer_mode.ui b/src/Mod/AddonManager/developer_mode.ui
index c0c63f03c8..c698b6d698 100644
--- a/src/Mod/AddonManager/developer_mode.ui
+++ b/src/Mod/AddonManager/developer_mode.ui
@@ -13,6 +13,9 @@
Addon Developer Tools
+
+ true
+
-
@@ -120,205 +123,7 @@
-
-
-
-
-
-
-
- 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
-
-
-
-
-
-
-
-
-
+
-
diff --git a/src/Mod/AddonManager/developer_mode_add_content.ui b/src/Mod/AddonManager/developer_mode_add_content.ui
index 68c5acf29f..3d8ae48f75 100644
--- a/src/Mod/AddonManager/developer_mode_add_content.ui
+++ b/src/Mod/AddonManager/developer_mode_add_content.ui
@@ -13,6 +13,9 @@
Content Item
+
+ true
+
-
@@ -312,205 +315,7 @@
-
-
-
-
-
-
-
- 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
-
-
-
-
-
-
-
-
-
+
-
diff --git a/src/Mod/AddonManager/developer_mode_licenses_table.ui b/src/Mod/AddonManager/developer_mode_licenses_table.ui
new file mode 100644
index 0000000000..c97a497eb1
--- /dev/null
+++ b/src/Mod/AddonManager/developer_mode_licenses_table.ui
@@ -0,0 +1,123 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+ Form
+
+
+
-
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/developer_mode_people_table.ui b/src/Mod/AddonManager/developer_mode_people_table.ui
new file mode 100644
index 0000000000..d878511c49
--- /dev/null
+++ b/src/Mod/AddonManager/developer_mode_people_table.ui
@@ -0,0 +1,116 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+ Form
+
+
+ -
+
+
+
+ 2
+ 0
+
+
+
+ People
+
+
+
-
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ 3
+
+
+ true
+
+
+ 75
+
+
+ true
+
+
+ false
+
+
+
+ Kind
+
+
+
+
+ Name
+
+
+
+
+ Email
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ ...
+
+
+ true
+
+
+
+ -
+
+
+ ...
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+