FreeCAD

Форк
0
/
addonmanager_devmode.py 
699 строк · 29.4 Кб
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
""" Classes to manage "Developer Mode" """
25

26
import os
27
import datetime
28
import subprocess
29

30
import FreeCAD
31
import FreeCADGui
32
from freecad.utils import get_python_exe
33

34
from PySide.QtWidgets import (
35
    QFileDialog,
36
    QListWidgetItem,
37
    QDialog,
38
    QSizePolicy,
39
    QMessageBox,
40
)
41
from PySide.QtGui import (
42
    QIcon,
43
    QPixmap,
44
)
45
from PySide.QtCore import Qt
46
from addonmanager_git import GitManager, NoGitFound
47

48
from addonmanager_devmode_add_content import AddContent
49
from addonmanager_devmode_validators import NameValidator, VersionValidator
50
from addonmanager_devmode_predictor import Predictor
51
from addonmanager_devmode_people_table import PeopleTable
52
from addonmanager_devmode_licenses_table import LicensesTable
53
import addonmanager_utilities as utils
54

55
translate = FreeCAD.Qt.translate
56

57
# pylint: disable=too-few-public-methods
58

59
ContentTypeRole = Qt.UserRole
60
ContentIndexRole = Qt.UserRole + 1
61

62

63
class AddonGitInterface:
64
    """Wrapper to handle the git calls needed by this class"""
65

66
    git_manager = None
67

68
    def __init__(self, path):
69
        self.git_exists = False
70
        if not AddonGitInterface.git_manager:
71
            try:
72
                AddonGitInterface.git_manager = GitManager()
73
            except NoGitFound:
74
                FreeCAD.Console.PrintLog("No git found, Addon Manager Developer Mode disabled.")
75
                return
76

77
        self.path = path
78
        if os.path.exists(os.path.join(path, ".git")):
79
            self.git_exists = True
80
            self.branch = AddonGitInterface.git_manager.current_branch(self.path)
81
            self.remote = AddonGitInterface.git_manager.get_remote(self.path)
82

83
    @property
84
    def branches(self):
85
        """The branches available for this repo."""
86
        if self.git_exists:
87
            return AddonGitInterface.git_manager.get_branches(self.path)
88
        return []
89

90
    @property
91
    def committers(self):
92
        """The committers to this repo, in the last ten commits"""
93
        if self.git_exists:
94
            return AddonGitInterface.git_manager.get_last_committers(self.path, 10)
95
        return []
96

97
    @property
98
    def authors(self):
99
        """The committers to this repo, in the last ten commits"""
100
        if self.git_exists:
101
            return AddonGitInterface.git_manager.get_last_authors(self.path, 10)
102
        return []
103

104

105
# pylint: disable=too-many-instance-attributes
106

107

108
class DeveloperMode:
109
    """The main Developer Mode dialog, for editing package.xml metadata graphically."""
110

111
    def __init__(self):
112
        # In the UI we want to show a translated string for the person type, but the underlying
113
        # string must be the one expected by the metadata parser, in English
114
        self.person_type_translation = {
115
            "maintainer": translate("AddonsInstaller", "Maintainer"),
116
            "author": translate("AddonsInstaller", "Author"),
117
        }
118
        self.dialog = FreeCADGui.PySideUic.loadUi(
119
            os.path.join(os.path.dirname(__file__), "developer_mode.ui")
120
        )
121
        self.people_table = PeopleTable()
122
        self.licenses_table = LicensesTable()
123
        large_size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
124
        large_size_policy.setHorizontalStretch(2)
125
        small_size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
126
        small_size_policy.setHorizontalStretch(1)
127
        self.people_table.widget.setSizePolicy(large_size_policy)
128
        self.licenses_table.widget.setSizePolicy(small_size_policy)
129
        self.dialog.peopleAndLicenseshorizontalLayout.addWidget(self.people_table.widget)
130
        self.dialog.peopleAndLicenseshorizontalLayout.addWidget(self.licenses_table.widget)
131
        self.pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
132
        self.current_mod: str = ""
133
        self.git_interface = None
134
        self.has_toplevel_icon = False
135
        self.metadata = None
136

137
        self._setup_dialog_signals()
138

139
        self.dialog.displayNameLineEdit.setValidator(NameValidator())
140
        self.dialog.versionLineEdit.setValidator(VersionValidator())
141
        self.dialog.minPythonLineEdit.setValidator(VersionValidator())
142

143
        self.dialog.addContentItemToolButton.setIcon(
144
            QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
145
        )
146
        self.dialog.removeContentItemToolButton.setIcon(
147
            QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
148
        )
149

150
    def show(self, parent=None, path: str = None):
151
        """Show the main dev mode dialog"""
152
        if parent:
153
            self.dialog.setParent(parent)
154
        if path and os.path.exists(path):
155
            self.dialog.pathToAddonComboBox.setEditText(path)
156
        elif self.pref.HasGroup("recentModsList"):
157
            recent_mods_group = self.pref.GetGroup("recentModsList")
158
            entry = recent_mods_group.GetString("Mod0", "")
159
            if entry:
160
                self._populate_dialog(entry)
161
                self._update_recent_mods(entry)
162
                self._populate_combo()
163
            else:
164
                self._clear_all_fields()
165
        else:
166
            self._clear_all_fields()
167

168
        result = self.dialog.exec()
169
        if result == QDialog.Accepted:
170
            self._sync_metadata_to_ui()
171
            now = datetime.date.today()
172
            self.metadata.Date = str(now)
173
            self.metadata.write(os.path.join(self.current_mod, "package.xml"))
174

175
    def _populate_dialog(self, path_to_repo: str):
176
        """Populate this dialog using the best available parsing of the contents of the repo at
177
        path_to_repo. This is a multi-layered process that starts with any existing package.xml
178
        file or other known metadata files, and proceeds through examining the contents of the
179
        directory structure."""
180
        self.current_mod = path_to_repo
181
        self._scan_for_git_info(self.current_mod)
182

183
        metadata_path = os.path.join(path_to_repo, "package.xml")
184
        if os.path.exists(metadata_path):
185
            try:
186
                self.metadata = FreeCAD.Metadata(metadata_path)
187
            except FreeCAD.Base.XMLBaseException as e:
188
                FreeCAD.Console.PrintError(
189
                    translate(
190
                        "AddonsInstaller",
191
                        "XML failure while reading metadata from file {}",
192
                    ).format(metadata_path)
193
                    + "\n\n"
194
                    + str(e)
195
                    + "\n\n"
196
                )
197
            except FreeCAD.Base.RuntimeError as e:
198
                FreeCAD.Console.PrintError(
199
                    translate("AddonsInstaller", "Invalid metadata in file {}").format(
200
                        metadata_path
201
                    )
202
                    + "\n\n"
203
                    + str(e)
204
                    + "\n\n"
205
                )
206

207
        self._clear_all_fields()
208

209
        if not self.metadata:
210
            self._predict_metadata()
211

212
        self.dialog.displayNameLineEdit.setText(self.metadata.Name)
213
        self.dialog.descriptionTextEdit.setPlainText(self.metadata.Description)
214
        self.dialog.versionLineEdit.setText(self.metadata.Version)
215
        self.dialog.minPythonLineEdit.setText(self.metadata.PythonMin)
216

217
        self._populate_urls_from_metadata(self.metadata)
218
        self._populate_contents_from_metadata(self.metadata)
219

220
        self._populate_icon_from_metadata(self.metadata)
221

222
        self.people_table.show(self.metadata)
223
        self.licenses_table.show(self.metadata, self.current_mod)
224

225
    def _populate_urls_from_metadata(self, metadata):
226
        """Use the passed metadata object to populate the urls"""
227
        for url in metadata.Urls:
228
            if url["type"] == "website":
229
                self.dialog.websiteURLLineEdit.setText(url["location"])
230
            elif url["type"] == "repository":
231
                self.dialog.repositoryURLLineEdit.setText(url["location"])
232
                branch_from_metadata = url["branch"]
233
                branch_from_local_path = self.git_interface.branch
234
                if branch_from_metadata != branch_from_local_path:
235
                    # pylint: disable=line-too-long
236
                    FreeCAD.Console.PrintWarning(
237
                        translate(
238
                            "AddonsInstaller",
239
                            "WARNING: Path specified in package.xml metadata does not match currently checked-out branch.",
240
                        )
241
                        + "\n"
242
                    )
243
                self.dialog.branchComboBox.setCurrentText(branch_from_metadata)
244
            elif url["type"] == "bugtracker":
245
                self.dialog.bugtrackerURLLineEdit.setText(url["location"])
246
            elif url["type"] == "readme":
247
                self.dialog.readmeURLLineEdit.setText(url["location"])
248
            elif url["type"] == "documentation":
249
                self.dialog.documentationURLLineEdit.setText(url["location"])
250
            elif url["type"] == "discussion":
251
                self.dialog.discussionURLLineEdit.setText(url["location"])
252

253
    def _populate_contents_from_metadata(self, metadata):
254
        """Use the passed metadata object to populate the contents list"""
255
        contents = metadata.Content
256
        self.dialog.contentsListWidget.clear()
257
        for content_type in contents:
258
            counter = 0
259
            for item in contents[content_type]:
260
                contents_string = f"[{content_type}] "
261
                info = []
262
                if item.Name:
263
                    info.append(translate("AddonsInstaller", "Name") + ": " + item.Name)
264
                if item.Classname:
265
                    info.append(translate("AddonsInstaller", "Class") + ": " + item.Classname)
266
                if item.Description:
267
                    info.append(
268
                        translate("AddonsInstaller", "Description") + ": " + item.Description
269
                    )
270
                if item.Subdirectory:
271
                    info.append(
272
                        translate("AddonsInstaller", "Subdirectory") + ": " + item.Subdirectory
273
                    )
274
                if item.File:
275
                    info.append(translate("AddonsInstaller", "Files") + ": " + ", ".join(item.File))
276
                contents_string += ", ".join(info)
277

278
                item = QListWidgetItem(contents_string)
279
                item.setData(ContentTypeRole, content_type)
280
                item.setData(ContentIndexRole, counter)
281
                self.dialog.contentsListWidget.addItem(item)
282
                counter += 1
283

284
    def _populate_icon_from_metadata(self, metadata):
285
        """Use the passed metadata object to populate the icon fields"""
286
        self.dialog.iconDisplayLabel.setPixmap(QPixmap())
287
        icon = metadata.Icon
288
        icon_path = None
289
        if icon:
290
            icon_path = os.path.join(self.current_mod, icon.replace("/", os.path.sep))
291
            self.has_toplevel_icon = True
292
        else:
293
            self.has_toplevel_icon = False
294
            contents = metadata.Content
295
            if "workbench" in contents:
296
                for wb in contents["workbench"]:
297
                    icon = wb.Icon
298
                    path = wb.Subdirectory
299
                    if icon:
300
                        icon_path = os.path.join(
301
                            self.current_mod, path, icon.replace("/", os.path.sep)
302
                        )
303
                        break
304

305
        if icon_path and os.path.isfile(icon_path):
306
            icon_data = QIcon(icon_path)
307
            if not icon_data.isNull():
308
                self.dialog.iconDisplayLabel.setPixmap(icon_data.pixmap(32, 32))
309
        self.dialog.iconPathLineEdit.setText(icon)
310

311
    def _predict_metadata(self):
312
        """If there is no metadata, try to guess at values for it"""
313
        self.metadata = FreeCAD.Metadata()
314
        predictor = Predictor()
315
        self.metadata = predictor.predict_metadata(self.current_mod)
316

317
    def _scan_for_git_info(self, path: str):
318
        """Look for branch availability"""
319
        self.git_interface = AddonGitInterface(path)
320
        if self.git_interface.git_exists:
321
            self.dialog.branchComboBox.clear()
322
            for branch in self.git_interface.branches:
323
                if branch and branch.startswith("origin/") and branch != "origin/HEAD":
324
                    self.dialog.branchComboBox.addItem(branch[len("origin/") :])
325
            self.dialog.branchComboBox.setCurrentText(self.git_interface.branch)
326

327
    def _clear_all_fields(self):
328
        """Clear out all fields"""
329
        self.dialog.displayNameLineEdit.clear()
330
        self.dialog.descriptionTextEdit.clear()
331
        self.dialog.versionLineEdit.clear()
332
        self.dialog.websiteURLLineEdit.clear()
333
        self.dialog.repositoryURLLineEdit.clear()
334
        self.dialog.bugtrackerURLLineEdit.clear()
335
        self.dialog.readmeURLLineEdit.clear()
336
        self.dialog.documentationURLLineEdit.clear()
337
        self.dialog.discussionURLLineEdit.clear()
338
        self.dialog.minPythonLineEdit.clear()
339
        self.dialog.iconDisplayLabel.setPixmap(QPixmap())
340
        self.dialog.iconPathLineEdit.clear()
341

342
    def _setup_dialog_signals(self):
343
        """Set up the signal and slot connections for the main dialog."""
344

345
        self.dialog.addonPathBrowseButton.clicked.connect(self._addon_browse_button_clicked)
346
        self.dialog.pathToAddonComboBox.editTextChanged.connect(self._addon_combo_text_changed)
347
        self.dialog.detectMinPythonButton.clicked.connect(self._detect_min_python_clicked)
348
        self.dialog.iconBrowseButton.clicked.connect(self._browse_for_icon_clicked)
349

350
        self.dialog.addContentItemToolButton.clicked.connect(self._add_content_clicked)
351
        self.dialog.removeContentItemToolButton.clicked.connect(self._remove_content_clicked)
352
        self.dialog.contentsListWidget.itemSelectionChanged.connect(self._content_selection_changed)
353
        self.dialog.contentsListWidget.itemDoubleClicked.connect(self._edit_content)
354

355
        self.dialog.versionToTodayButton.clicked.connect(self._set_to_today_clicked)
356

357
        # Finally, populate the combo boxes, etc.
358
        self._populate_combo()
359

360
        # Disable all the "Remove" buttons until something is selected
361
        self.dialog.removeContentItemToolButton.setDisabled(True)
362

363
    def _sync_metadata_to_ui(self):
364
        """Take the data from the UI fields and put it into the stored metadata
365
        object. Only overwrites known data fields: unknown metadata will be retained."""
366

367
        if not self.metadata:
368
            self.metadata = FreeCAD.Metadata()
369

370
        self.metadata.Name = self.dialog.displayNameLineEdit.text()
371
        self.metadata.Description = self.dialog.descriptionTextEdit.document().toPlainText()
372
        self.metadata.Version = self.dialog.versionLineEdit.text()
373
        self.metadata.Icon = self.dialog.iconPathLineEdit.text()
374

375
        urls = []
376
        if self.dialog.websiteURLLineEdit.text():
377
            urls.append({"location": self.dialog.websiteURLLineEdit.text(), "type": "website"})
378
        if self.dialog.repositoryURLLineEdit.text():
379
            urls.append(
380
                {
381
                    "location": self.dialog.repositoryURLLineEdit.text(),
382
                    "type": "repository",
383
                    "branch": self.dialog.branchComboBox.currentText(),
384
                }
385
            )
386
        if self.dialog.bugtrackerURLLineEdit.text():
387
            urls.append(
388
                {
389
                    "location": self.dialog.bugtrackerURLLineEdit.text(),
390
                    "type": "bugtracker",
391
                }
392
            )
393
        if self.dialog.readmeURLLineEdit.text():
394
            urls.append({"location": self.dialog.readmeURLLineEdit.text(), "type": "readme"})
395
        if self.dialog.documentationURLLineEdit.text():
396
            urls.append(
397
                {
398
                    "location": self.dialog.documentationURLLineEdit.text(),
399
                    "type": "documentation",
400
                }
401
            )
402
        if self.dialog.discussionURLLineEdit.text():
403
            urls.append(
404
                {
405
                    "location": self.dialog.discussionURLLineEdit.text(),
406
                    "type": "discussion",
407
                }
408
            )
409
        self.metadata.Urls = urls
410

411
        if self.dialog.minPythonLineEdit.text():
412
            self.metadata.PythonMin = self.dialog.minPythonLineEdit.text()
413
        else:
414
            self.metadata.PythonMin = "0.0.0"  # Code for "unset"
415

416
        # Content, people, and licenses should already be synchronized
417

418
    ###############################################################################################
419
    #                                         DIALOG SLOTS
420
    ###############################################################################################
421

422
    def _addon_browse_button_clicked(self):
423
        """Launch a modal file/folder selection dialog -- if something is selected, it is
424
        processed by the parsing code and used to fill in the contents of the rest of the
425
        dialog."""
426

427
        start_dir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod")
428
        mod_dir = QFileDialog.getExistingDirectory(
429
            parent=self.dialog,
430
            caption=translate(
431
                "AddonsInstaller",
432
                "Select the folder containing your Addon",
433
            ),
434
            dir=start_dir,
435
        )
436

437
        if mod_dir and os.path.exists(mod_dir):
438
            self.dialog.pathToAddonComboBox.setEditText(mod_dir)
439

440
    def _addon_combo_text_changed(self, new_text: str):
441
        """Called when the text is changed, either because it was directly edited, or because
442
        a new item was selected."""
443
        if new_text == self.current_mod:
444
            # It doesn't look like it actually changed, bail out
445
            return
446
        self.metadata = None
447
        self._clear_all_fields()
448
        if not os.path.exists(new_text):
449
            # This isn't a thing (Yet. Maybe the user is still typing?)
450
            return
451
        self._populate_dialog(new_text)
452
        self._update_recent_mods(new_text)
453
        self._populate_combo()
454

455
    def _populate_combo(self):
456
        """Fill in the combo box with the values from the stored recent mods list, selecting the
457
        top one. Does not trigger any signals."""
458
        combo = self.dialog.pathToAddonComboBox
459
        combo.blockSignals(True)
460
        recent_mods_group = self.pref.GetGroup("recentModsList")
461
        recent_mods = set()
462
        combo.clear()
463
        for i in range(10):
464
            entry_name = f"Mod{i}"
465
            mod = recent_mods_group.GetString(entry_name, "None")
466
            if mod != "None" and mod not in recent_mods and os.path.exists(mod):
467
                recent_mods.add(mod)
468
                combo.addItem(mod)
469
        if recent_mods:
470
            combo.setCurrentIndex(0)
471
        combo.blockSignals(False)
472

473
    def _update_recent_mods(self, path):
474
        """Update the list of recent mods, storing at most ten, with path at the top of the
475
        list."""
476
        recent_mod_paths = [path]
477
        if self.pref.HasGroup("recentModsList"):
478
            recent_mods_group = self.pref.GetGroup("recentModsList")
479

480
            # This group has a maximum of ten entries, sorted by last-accessed date
481
            for i in range(0, 10):
482
                entry_name = f"Mod{i}"
483
                entry = recent_mods_group.GetString(entry_name, "")
484
                if entry and entry not in recent_mod_paths and os.path.exists(entry):
485
                    recent_mod_paths.append(entry)
486

487
            # Remove the whole thing, so we can recreate it from scratch
488
            self.pref.RemGroup("recentModsList")
489

490
        if recent_mod_paths:
491
            recent_mods_group = self.pref.GetGroup("recentModsList")
492
            for i, mod in zip(range(10), recent_mod_paths):
493
                entry_name = f"Mod{i}"
494
                recent_mods_group.SetString(entry_name, mod)
495

496
    def _add_content_clicked(self):
497
        """Callback: The Add Content button was clicked"""
498
        dlg = AddContent(self.current_mod, self.metadata)
499
        singleton = False
500
        if self.dialog.contentsListWidget.count() == 0:
501
            singleton = True
502
        content_type, new_metadata = dlg.exec(singleton=singleton)
503
        if content_type and new_metadata:
504
            self.metadata.addContentItem(content_type, new_metadata)
505
            self._populate_contents_from_metadata(self.metadata)
506

507
    def _remove_content_clicked(self):
508
        """Callback: the remove content button was clicked"""
509

510
        item = self.dialog.contentsListWidget.currentItem()
511
        if not item:
512
            return
513
        content_type = item.data(ContentTypeRole)
514
        content_index = item.data(ContentIndexRole)
515
        if self.metadata.Content[content_type] and content_index < len(
516
            self.metadata.Content[content_type]
517
        ):
518
            content_name = self.metadata.Content[content_type][content_index].Name
519
            self.metadata.removeContentItem(content_type, content_name)
520
            self._populate_contents_from_metadata(self.metadata)
521

522
    def _content_selection_changed(self):
523
        """Callback: the selected content item changed"""
524
        items = self.dialog.contentsListWidget.selectedItems()
525
        if items:
526
            self.dialog.removeContentItemToolButton.setDisabled(False)
527
        else:
528
            self.dialog.removeContentItemToolButton.setDisabled(True)
529

530
    def _edit_content(self, item):
531
        """Callback: a content row was double-clicked"""
532
        dlg = AddContent(self.current_mod, self.metadata)
533

534
        content_type = item.data(ContentTypeRole)
535
        content_index = item.data(ContentIndexRole)
536

537
        content = self.metadata.Content
538
        metadata = content[content_type][content_index]
539
        old_name = metadata.Name
540
        new_type, new_metadata = dlg.exec(content_type, metadata, len(content) == 1)
541
        if new_type and new_metadata:
542
            self.metadata.removeContentItem(content_type, old_name)
543
            self.metadata.addContentItem(new_type, new_metadata)
544
            self._populate_contents_from_metadata(self.metadata)
545

546
    def _set_to_today_clicked(self):
547
        """Callback: the "set to today" button was clicked"""
548
        year = datetime.date.today().year
549
        month = datetime.date.today().month
550
        day = datetime.date.today().day
551
        version_string = f"{year}.{month:>02}.{day:>02}"
552
        self.dialog.versionLineEdit.setText(version_string)
553

554
    def _detect_min_python_clicked(self):
555
        if not self._ensure_vermin_loaded():
556
            FreeCAD.Console.PrintWarning(
557
                translate(
558
                    "AddonsInstaller",
559
                    "No Vermin, cancelling operation.",
560
                    "NOTE: Vermin is a Python package and proper noun - do not translate",
561
                )
562
                + "\n"
563
            )
564
            return
565
        FreeCAD.Console.PrintMessage(
566
            translate("AddonsInstaller", "Scanning Addon for Python version compatibility")
567
            + "...\n"
568
        )
569
        # pylint: disable=import-outside-toplevel
570
        import vermin
571

572
        required_minor_version = 0
573
        for dir_path, _, filenames in os.walk(self.current_mod):
574
            for filename in filenames:
575
                if filename.endswith(".py"):
576
                    with open(os.path.join(dir_path, filename), encoding="utf-8") as f:
577
                        contents = f.read()
578
                        version_strings = vermin.version_strings(vermin.detect(contents))
579
                        version = version_strings.split(",")
580
                        if len(version) >= 2:
581
                            # Only care about Py3, and only if there is a dot in the version:
582
                            if "." in version[1]:
583
                                py3 = version[1].split(".")
584
                                major = int(py3[0].strip())
585
                                minor = int(py3[1].strip())
586
                                if major == 3:
587
                                    FreeCAD.Console.PrintLog(
588
                                        f"Detected Python 3.{minor} required by {filename}\n"
589
                                    )
590
                                    required_minor_version = max(required_minor_version, minor)
591
        self.dialog.minPythonLineEdit.setText(f"3.{required_minor_version}")
592
        QMessageBox.information(
593
            self.dialog,
594
            translate("AddonsInstaller", "Minimum Python Version Detected"),
595
            translate(
596
                "AddonsInstaller",
597
                "Vermin auto-detected a required version of Python 3.{}",
598
            ).format(required_minor_version),
599
            QMessageBox.Ok,
600
        )
601

602
    def _ensure_vermin_loaded(self) -> bool:
603
        try:
604
            # pylint: disable=import-outside-toplevel,unused-import
605
            import vermin
606
        except ImportError:
607
            # pylint: disable=line-too-long
608
            response = QMessageBox.question(
609
                self.dialog,
610
                translate("AddonsInstaller", "Install Vermin?"),
611
                translate(
612
                    "AddonsInstaller",
613
                    "Auto-detecting the required version of Python for this Addon requires Vermin (https://pypi.org/project/vermin/). OK to install?",
614
                ),
615
                QMessageBox.Yes | QMessageBox.Cancel,
616
            )
617
            if response == QMessageBox.Cancel:
618
                return False
619
            FreeCAD.Console.PrintMessage(
620
                translate("AddonsInstaller", "Attempting to install Vermin from PyPi") + "...\n"
621
            )
622
            python_exe = get_python_exe()
623
            vendor_path = os.path.join(FreeCAD.getUserAppDataDir(), "AdditionalPythonPackages")
624
            if not os.path.exists(vendor_path):
625
                os.makedirs(vendor_path)
626

627
            proc = subprocess.run(
628
                [
629
                    python_exe,
630
                    "-m",
631
                    "pip",
632
                    "install",
633
                    "--disable-pip-version-check",
634
                    "--target",
635
                    vendor_path,
636
                    "vermin",
637
                ],
638
                capture_output=True,
639
                check=True,
640
            )
641
            FreeCAD.Console.PrintMessage(proc.stdout.decode())
642
            if proc.returncode != 0:
643
                QMessageBox.critical(
644
                    self.dialog,
645
                    translate("AddonsInstaller", "Installation failed"),
646
                    translate(
647
                        "AddonsInstaller",
648
                        "Failed to install Vermin -- check Report View for details.",
649
                        "'Vermin' is the name of a Python package, do not translate",
650
                    ),
651
                    QMessageBox.Cancel,
652
                )
653
                return False
654
        try:
655
            # pylint: disable=import-outside-toplevel
656
            import vermin
657
        except ImportError:
658
            QMessageBox.critical(
659
                self.dialog,
660
                translate("AddonsInstaller", "Installation failed"),
661
                translate(
662
                    "AddonsInstaller",
663
                    "Failed to import vermin after installation -- cannot scan Addon.",
664
                    "'vermin' is the name of a Python package, do not translate",
665
                ),
666
                QMessageBox.Cancel,
667
            )
668
            return False
669
        return True
670

671
    def _browse_for_icon_clicked(self):
672
        """Callback: when the "Browse..." button for the icon field is clicked"""
673
        new_icon_path, _ = QFileDialog.getOpenFileName(
674
            parent=self.dialog,
675
            caption=translate(
676
                "AddonsInstaller",
677
                "Select an icon file for this package",
678
            ),
679
            dir=self.current_mod,
680
        )
681

682
        if not new_icon_path:
683
            return
684

685
        base_path = self.current_mod.replace("/", os.path.sep)
686
        icon_path = new_icon_path.replace("/", os.path.sep)
687
        if base_path[-1] != os.path.sep:
688
            base_path += os.path.sep
689

690
        if not icon_path.startswith(base_path):
691
            FreeCAD.Console.PrintError(
692
                translate("AddonsInstaller", "{} is not a subdirectory of {}").format(
693
                    new_icon_path, self.current_mod
694
                )
695
                + "\n"
696
            )
697
            return
698
        self.metadata.Icon = new_icon_path[len(base_path) :]
699
        self._populate_icon_from_metadata(self.metadata)
700

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.