freecad-cam/Mod/AddonManager/addonmanager_devmode_license_selector.py
2026-02-01 01:59:24 +01:00

276 lines
12 KiB
Python

# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * 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 class to manage selection of a license for an Addon. """
import os
from datetime import date
from typing import Optional, Tuple
import FreeCAD
import FreeCADGui
from PySide.QtWidgets import QFileDialog, QDialog
from PySide.QtGui import QDesktopServices
from PySide.QtCore import QUrl, QFile, QIODevice
try:
from PySide.QtGui import (
QRegularExpressionValidator,
)
from PySide.QtCore import QRegularExpression
RegexWrapper = QRegularExpression
RegexValidatorWrapper = QRegularExpressionValidator
except ImportError:
QRegularExpressionValidator = None
QRegularExpression = None
from PySide.QtGui import (
QRegExpValidator,
)
from PySide.QtCore import QRegExp
RegexWrapper = QRegExp
RegexValidatorWrapper = QRegExpValidator
translate = FreeCAD.Qt.translate
class LicenseSelector:
"""Choose from a selection of licenses, or provide your own. Includes the capability to create
the license file itself for a variety of popular open-source licenses, as well as providing
links to opensource.org's page about the various licenses (which often link to other resources).
"""
licenses = {
"Apache-2.0": (
"Apache License, Version 2.0",
"https://opensource.org/licenses/Apache-2.0",
),
"BSD-2-Clause": (
"The 2-Clause BSD License",
"https://opensource.org/licenses/BSD-2-Clause",
),
"BSD-3-Clause": (
"The 3-Clause BSD License",
"https://opensource.org/licenses/BSD-3-Clause",
),
"CC0-1.0": (
"No Rights Reserved/Public Domain",
"https://creativecommons.org/choose/zero/",
),
"GPL-2.0-or-later": (
"GNU General Public License version 2",
"https://opensource.org/licenses/GPL-2.0",
),
"GPL-3.0-or-later": (
"GNU General Public License version 3",
"https://opensource.org/licenses/GPL-3.0",
),
"LGPL-2.1-or-later": (
"GNU Lesser General Public License version 2.1",
"https://opensource.org/licenses/LGPL-2.1",
),
"LGPL-3.0-or-later": (
"GNU Lesser General Public License version 3",
"https://opensource.org/licenses/LGPL-3.0",
),
"MIT": (
"The MIT License",
"https://opensource.org/licenses/MIT",
),
"MPL-2.0": (
"Mozilla Public License 2.0",
"https://opensource.org/licenses/MPL-2.0",
),
}
def __init__(self, path_to_addon):
self.other_label = translate(
"AddonsInstaller",
"Other...",
"For providing a license other than one listed",
)
self.path_to_addon = path_to_addon
self.dialog = FreeCADGui.PySideUic.loadUi(
os.path.join(os.path.dirname(__file__), "developer_mode_license.ui")
)
self.pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
for short_code, details in LicenseSelector.licenses.items():
self.dialog.comboBox.addItem(f"{short_code}: {details[0]}", userData=short_code)
self.dialog.comboBox.addItem(self.other_label)
self.dialog.otherLineEdit.hide()
self.dialog.otherLabel.hide()
# Connections:
self.dialog.comboBox.currentIndexChanged.connect(self._selection_changed)
self.dialog.aboutButton.clicked.connect(self._about_clicked)
self.dialog.browseButton.clicked.connect(self._browse_clicked)
self.dialog.createButton.clicked.connect(self._create_clicked)
# Set up the first selection to whatever the user chose last time
short_code = self.pref.GetString("devModeLastSelectedLicense", "LGPL-2.1-or-later")
self.set_license(short_code)
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."""
if short_code:
self.set_license(short_code)
self.dialog.pathLineEdit.setText(license_path)
result = self.dialog.exec()
if result == QDialog.Accepted:
new_short_code = self.dialog.comboBox.currentData()
new_license_path = self.dialog.pathLineEdit.text()
if not new_short_code:
new_short_code = self.dialog.otherLineEdit.text()
self.pref.SetString("devModeLastSelectedLicense", new_short_code)
return new_short_code, new_license_path
return None
def set_license(self, short_code):
"""Set the currently-selected license."""
index = self.dialog.comboBox.findData(short_code)
if index != -1:
self.dialog.comboBox.setCurrentIndex(index)
else:
self.dialog.comboBox.setCurrentText(self.other_label)
self.dialog.otherLineEdit.setText(short_code)
def _selection_changed(self, _: int):
"""Callback: when the license selection changes, the UI is updated here."""
if self.dialog.comboBox.currentText() == self.other_label:
self.dialog.otherLineEdit.clear()
self.dialog.otherLineEdit.show()
self.dialog.otherLabel.show()
self.dialog.aboutButton.setDisabled(True)
else:
self.dialog.otherLineEdit.hide()
self.dialog.otherLabel.hide()
self.dialog.aboutButton.setDisabled(False)
def _current_short_code(self) -> str:
"""Gets the currently-selected license short code"""
short_code = self.dialog.comboBox.currentData()
if not short_code:
short_code = self.dialog.otherLineEdit.text()
return short_code
def _about_clicked(self):
"""Callback: when the About button is clicked, try to launch a system-default web browser
and display the OSI page about the currently-selected license."""
short_code = self.dialog.comboBox.currentData()
if short_code in LicenseSelector.licenses:
url = LicenseSelector.licenses[short_code][1]
QDesktopServices.openUrl(QUrl(url))
else:
FreeCAD.Console.PrintWarning(
f"Internal Error: unrecognized license short code {short_code}\n"
)
def _browse_clicked(self):
"""Callback: browse for an existing license file."""
start_dir = os.path.join(
self.path_to_addon,
self.dialog.pathLineEdit.text().replace("/", os.path.sep),
)
license_path, _ = QFileDialog.getOpenFileName(
parent=self.dialog,
caption=translate(
"AddonsInstaller",
"Select the corresponding license file in your Addon",
),
dir=start_dir,
)
if license_path:
self._set_path(self.path_to_addon, license_path)
def _set_path(self, start_dir: str, license_path: str):
"""Sets the value displayed in the path widget to the relative path from
start_dir to license_path"""
license_path = license_path.replace("/", os.path.sep)
base_dir = start_dir.replace("/", os.path.sep)
if base_dir[-1] != os.path.sep:
base_dir += os.path.sep
if not license_path.startswith(base_dir):
FreeCAD.Console.PrintError("Selected file not in Addon\n")
# Eventually offer to copy it?
return
relative_path = license_path[len(base_dir) :]
relative_path = relative_path.replace(os.path.sep, "/")
self.dialog.pathLineEdit.setText(relative_path)
def _create_clicked(self):
"""Asks the users for the path to save the new license file to, then copies our internal
copy of the license text to that file."""
start_dir = os.path.join(
self.path_to_addon,
self.dialog.pathLineEdit.text().replace("/", os.path.sep),
)
license_path, _ = QFileDialog.getSaveFileName(
parent=self.dialog,
caption=translate(
"AddonsInstaller",
"Location for new license file",
),
dir=os.path.join(start_dir, "LICENSE"),
)
if license_path:
self._set_path(start_dir, license_path)
short_code = self._current_short_code()
qf = QFile(f":/licenses/{short_code}.txt")
if qf.exists():
qf.open(QIODevice.ReadOnly)
byte_data = qf.readAll()
qf.close()
string_data = str(byte_data, encoding="utf-8")
if "<%%YEAR%%>" in string_data or "<%%COPYRIGHT HOLDER%%>" in string_data:
info_dlg = FreeCADGui.PySideUic.loadUi(
os.path.join(
os.path.dirname(__file__),
"developer_mode_copyright_info.ui",
)
)
info_dlg.yearLineEdit.setValidator(
RegexValidatorWrapper(RegexWrapper("^[12]\\d{3}$"))
)
info_dlg.yearLineEdit.setText(str(date.today().year))
result = info_dlg.exec()
if result != QDialog.Accepted:
return # Don't create the file, just bail out
holder = info_dlg.copyrightHolderLineEdit.text()
year = info_dlg.yearLineEdit.text()
string_data = string_data.replace("<%%YEAR%%>", year)
string_data = string_data.replace("<%%COPYRIGHT HOLDER%%>", holder)
with open(license_path, "w", encoding="utf-8") as f:
f.write(string_data)
else:
FreeCAD.Console.PrintError(f"Cannot create license file of type {short_code}\n")