276 lines
12 KiB
Python
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")
|