Addon Manager: Refactor Licenses and People tables

This commit is contained in:
Chris Hennes
2022-09-08 17:55:22 -05:00
parent 9348486a1f
commit b3dd3b6d00
16 changed files with 1071 additions and 834 deletions

View File

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

View File

@@ -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

View File

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

View File

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

View File

@@ -19,4 +19,3 @@
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************

View File

@@ -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."""

View File

@@ -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 *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
""" 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)

View File

@@ -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 *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
""" 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)

View File

@@ -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
return

View File

@@ -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)
return (QValidator.Invalid, value, position)

View File

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

View File

@@ -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

View File

@@ -13,6 +13,9 @@
<property name="windowTitle">
<string>Addon Developer Tools</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="mainVBoxLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayoutAddonPath">
@@ -120,205 +123,7 @@
</layout>
</item>
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="peopleAndLicenseshorizontalLayout">
<item>
<widget class="QGroupBox" name="peopleGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>People</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutPeople">
<item>
<widget class="QTableWidget" name="peopleTableWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="columnCount">
<number>3</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>75</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Kind</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Email</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutPeople">
<item>
<spacer name="horizontalSpacerPeople">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addPersonToolButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removePersonToolButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="licensesGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Licenses</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutLicenses">
<item>
<widget class="QTableWidget" name="licensesTableWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>60</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>License</string>
</property>
</column>
<column>
<property name="text">
<string>License file</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutLicenses">
<item>
<spacer name="horizontalSpacer_Licenses">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addLicenseToolButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeLicenseToolButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
<layout class="QHBoxLayout" name="peopleAndLicenseshorizontalLayout"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="labelRepoURL">

View File

@@ -13,6 +13,9 @@
<property name="windowTitle">
<string>Content Item</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
@@ -312,205 +315,7 @@
</widget>
</item>
<item row="4" column="0" colspan="2">
<layout class="QHBoxLayout" name="peopleAndLicenseshorizontalLayout">
<item>
<widget class="QGroupBox" name="peopleGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>People</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutPeople">
<item>
<widget class="QTableWidget" name="peopleTableWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="columnCount">
<number>3</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>75</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Kind</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Email</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutPeople">
<item>
<spacer name="horizontalSpacerPeople">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addPersonToolButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removePersonToolButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="licensesGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Licenses</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutLicenses">
<item>
<widget class="QTableWidget" name="licensesTableWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>60</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>License</string>
</property>
</column>
<column>
<property name="text">
<string>License file</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutLicenses">
<item>
<spacer name="horizontalSpacer_Licenses">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addLicenseToolButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeLicenseToolButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
<layout class="QHBoxLayout" name="peopleAndLicenseshorizontalLayout"/>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_6">

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Licenses</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutLicenses">
<item>
<widget class="QTableWidget" name="tableWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>60</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>License</string>
</property>
</column>
<column>
<property name="text">
<string>License file</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="hLayout">
<item>
<spacer name="hSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>People</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutPeople">
<item>
<widget class="QTableWidget" name="tableWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="columnCount">
<number>3</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>75</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Kind</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Email</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="hLayout">
<item>
<spacer name="hSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeButton">
<property name="text">
<string>...</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>