# 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 * # * . * # * * # *************************************************************************** """ 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")