FreeCAD
275 строк · 11.5 Кб
1# SPDX-License-Identifier: LGPL-2.1-or-later
2# ***************************************************************************
3# * *
4# * Copyright (c) 2022 FreeCAD Project Association *
5# * *
6# * This file is part of FreeCAD. *
7# * *
8# * FreeCAD is free software: you can redistribute it and/or modify it *
9# * under the terms of the GNU Lesser General Public License as *
10# * published by the Free Software Foundation, either version 2.1 of the *
11# * License, or (at your option) any later version. *
12# * *
13# * FreeCAD is distributed in the hope that it will be useful, but *
14# * WITHOUT ANY WARRANTY; without even the implied warranty of *
15# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
16# * Lesser General Public License for more details. *
17# * *
18# * You should have received a copy of the GNU Lesser General Public *
19# * License along with FreeCAD. If not, see *
20# * <https://www.gnu.org/licenses/>. *
21# * *
22# ***************************************************************************
23
24""" Contains a class to manage selection of a license for an Addon. """
25
26import os
27from datetime import date
28from typing import Optional, Tuple
29
30import FreeCAD
31import FreeCADGui
32
33from PySide.QtWidgets import QFileDialog, QDialog
34from PySide.QtGui import QDesktopServices
35from PySide.QtCore import QUrl, QFile, QIODevice
36
37try:
38from PySide.QtGui import (
39QRegularExpressionValidator,
40)
41from PySide.QtCore import QRegularExpression
42
43RegexWrapper = QRegularExpression
44RegexValidatorWrapper = QRegularExpressionValidator
45except ImportError:
46QRegularExpressionValidator = None
47QRegularExpression = None
48from PySide.QtGui import (
49QRegExpValidator,
50)
51from PySide.QtCore import QRegExp
52
53RegexWrapper = QRegExp
54RegexValidatorWrapper = QRegExpValidator
55
56translate = FreeCAD.Qt.translate
57
58
59class LicenseSelector:
60"""Choose from a selection of licenses, or provide your own. Includes the capability to create
61the license file itself for a variety of popular open-source licenses, as well as providing
62links to opensource.org's page about the various licenses (which often link to other resources).
63"""
64
65licenses = {
66"Apache-2.0": (
67"Apache License, Version 2.0",
68"https://opensource.org/licenses/Apache-2.0",
69),
70"BSD-2-Clause": (
71"The 2-Clause BSD License",
72"https://opensource.org/licenses/BSD-2-Clause",
73),
74"BSD-3-Clause": (
75"The 3-Clause BSD License",
76"https://opensource.org/licenses/BSD-3-Clause",
77),
78"CC0-1.0": (
79"No Rights Reserved/Public Domain",
80"https://creativecommons.org/choose/zero/",
81),
82"GPL-2.0-or-later": (
83"GNU General Public License version 2",
84"https://opensource.org/licenses/GPL-2.0",
85),
86"GPL-3.0-or-later": (
87"GNU General Public License version 3",
88"https://opensource.org/licenses/GPL-3.0",
89),
90"LGPL-2.1-or-later": (
91"GNU Lesser General Public License version 2.1",
92"https://opensource.org/licenses/LGPL-2.1",
93),
94"LGPL-3.0-or-later": (
95"GNU Lesser General Public License version 3",
96"https://opensource.org/licenses/LGPL-3.0",
97),
98"MIT": (
99"The MIT License",
100"https://opensource.org/licenses/MIT",
101),
102"MPL-2.0": (
103"Mozilla Public License 2.0",
104"https://opensource.org/licenses/MPL-2.0",
105),
106}
107
108def __init__(self, path_to_addon):
109self.other_label = translate(
110"AddonsInstaller",
111"Other...",
112"For providing a license other than one listed",
113)
114self.path_to_addon = path_to_addon
115self.dialog = FreeCADGui.PySideUic.loadUi(
116os.path.join(os.path.dirname(__file__), "developer_mode_license.ui")
117)
118self.pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
119for short_code, details in LicenseSelector.licenses.items():
120self.dialog.comboBox.addItem(f"{short_code}: {details[0]}", userData=short_code)
121self.dialog.comboBox.addItem(self.other_label)
122self.dialog.otherLineEdit.hide()
123self.dialog.otherLabel.hide()
124
125# Connections:
126self.dialog.comboBox.currentIndexChanged.connect(self._selection_changed)
127self.dialog.aboutButton.clicked.connect(self._about_clicked)
128self.dialog.browseButton.clicked.connect(self._browse_clicked)
129self.dialog.createButton.clicked.connect(self._create_clicked)
130
131# Set up the first selection to whatever the user chose last time
132short_code = self.pref.GetString("devModeLastSelectedLicense", "LGPL-2.1-or-later")
133self.set_license(short_code)
134
135def exec(self, short_code: str = None, license_path: str = "") -> Optional[Tuple[str, str]]:
136"""The main method for executing this dialog, as a modal that returns a tuple of the
137license's "short code" and optionally the path to the license file. Returns a tuple
138of None,None if the user cancels the operation."""
139
140if short_code:
141self.set_license(short_code)
142self.dialog.pathLineEdit.setText(license_path)
143result = self.dialog.exec()
144if result == QDialog.Accepted:
145new_short_code = self.dialog.comboBox.currentData()
146new_license_path = self.dialog.pathLineEdit.text()
147if not new_short_code:
148new_short_code = self.dialog.otherLineEdit.text()
149self.pref.SetString("devModeLastSelectedLicense", new_short_code)
150return new_short_code, new_license_path
151return None
152
153def set_license(self, short_code):
154"""Set the currently-selected license."""
155index = self.dialog.comboBox.findData(short_code)
156if index != -1:
157self.dialog.comboBox.setCurrentIndex(index)
158else:
159self.dialog.comboBox.setCurrentText(self.other_label)
160self.dialog.otherLineEdit.setText(short_code)
161
162def _selection_changed(self, _: int):
163"""Callback: when the license selection changes, the UI is updated here."""
164if self.dialog.comboBox.currentText() == self.other_label:
165self.dialog.otherLineEdit.clear()
166self.dialog.otherLineEdit.show()
167self.dialog.otherLabel.show()
168self.dialog.aboutButton.setDisabled(True)
169else:
170self.dialog.otherLineEdit.hide()
171self.dialog.otherLabel.hide()
172self.dialog.aboutButton.setDisabled(False)
173
174def _current_short_code(self) -> str:
175"""Gets the currently-selected license short code"""
176short_code = self.dialog.comboBox.currentData()
177if not short_code:
178short_code = self.dialog.otherLineEdit.text()
179return short_code
180
181def _about_clicked(self):
182"""Callback: when the About button is clicked, try to launch a system-default web browser
183and display the OSI page about the currently-selected license."""
184short_code = self.dialog.comboBox.currentData()
185if short_code in LicenseSelector.licenses:
186url = LicenseSelector.licenses[short_code][1]
187QDesktopServices.openUrl(QUrl(url))
188else:
189FreeCAD.Console.PrintWarning(
190f"Internal Error: unrecognized license short code {short_code}\n"
191)
192
193def _browse_clicked(self):
194"""Callback: browse for an existing license file."""
195start_dir = os.path.join(
196self.path_to_addon,
197self.dialog.pathLineEdit.text().replace("/", os.path.sep),
198)
199license_path, _ = QFileDialog.getOpenFileName(
200parent=self.dialog,
201caption=translate(
202"AddonsInstaller",
203"Select the corresponding license file in your Addon",
204),
205dir=start_dir,
206)
207if license_path:
208self._set_path(self.path_to_addon, license_path)
209
210def _set_path(self, start_dir: str, license_path: str):
211"""Sets the value displayed in the path widget to the relative path from
212start_dir to license_path"""
213license_path = license_path.replace("/", os.path.sep)
214base_dir = start_dir.replace("/", os.path.sep)
215if base_dir[-1] != os.path.sep:
216base_dir += os.path.sep
217if not license_path.startswith(base_dir):
218FreeCAD.Console.PrintError("Selected file not in Addon\n")
219# Eventually offer to copy it?
220return
221relative_path = license_path[len(base_dir) :]
222relative_path = relative_path.replace(os.path.sep, "/")
223self.dialog.pathLineEdit.setText(relative_path)
224
225def _create_clicked(self):
226"""Asks the users for the path to save the new license file to, then copies our internal
227copy of the license text to that file."""
228start_dir = os.path.join(
229self.path_to_addon,
230self.dialog.pathLineEdit.text().replace("/", os.path.sep),
231)
232license_path, _ = QFileDialog.getSaveFileName(
233parent=self.dialog,
234caption=translate(
235"AddonsInstaller",
236"Location for new license file",
237),
238dir=os.path.join(start_dir, "LICENSE"),
239)
240if license_path:
241self._set_path(start_dir, license_path)
242short_code = self._current_short_code()
243qf = QFile(f":/licenses/{short_code}.txt")
244if qf.exists():
245qf.open(QIODevice.ReadOnly)
246byte_data = qf.readAll()
247qf.close()
248
249string_data = str(byte_data, encoding="utf-8")
250
251if "<%%YEAR%%>" in string_data or "<%%COPYRIGHT HOLDER%%>" in string_data:
252info_dlg = FreeCADGui.PySideUic.loadUi(
253os.path.join(
254os.path.dirname(__file__),
255"developer_mode_copyright_info.ui",
256)
257)
258info_dlg.yearLineEdit.setValidator(
259RegexValidatorWrapper(RegexWrapper("^[12]\\d{3}$"))
260)
261info_dlg.yearLineEdit.setText(str(date.today().year))
262result = info_dlg.exec()
263if result != QDialog.Accepted:
264return # Don't create the file, just bail out
265
266holder = info_dlg.copyrightHolderLineEdit.text()
267year = info_dlg.yearLineEdit.text()
268
269string_data = string_data.replace("<%%YEAR%%>", year)
270string_data = string_data.replace("<%%COPYRIGHT HOLDER%%>", holder)
271
272with open(license_path, "w", encoding="utf-8") as f:
273f.write(string_data)
274else:
275FreeCAD.Console.PrintError(f"Cannot create license file of type {short_code}\n")
276