FreeCAD

Форк
0
/
AddonManager.py 
934 строки · 39.1 Кб
1
#!/usr/bin/env python3
2

3
# SPDX-License-Identifier: LGPL-2.1-or-later
4
# ***************************************************************************
5
# *                                                                         *
6
# *   Copyright (c) 2022-2023 FreeCAD Project Association                   *
7
# *   Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net>              *
8
# *                                                                         *
9
# *   This file is part of FreeCAD.                                         *
10
# *                                                                         *
11
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
12
# *   under the terms of the GNU Lesser General Public License as           *
13
# *   published by the Free Software Foundation, either version 2.1 of the  *
14
# *   License, or (at your option) any later version.                       *
15
# *                                                                         *
16
# *   FreeCAD is distributed in the hope that it will be useful, but        *
17
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
18
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
19
# *   Lesser General Public License for more details.                       *
20
# *                                                                         *
21
# *   You should have received a copy of the GNU Lesser General Public      *
22
# *   License along with FreeCAD. If not, see                               *
23
# *   <https://www.gnu.org/licenses/>.                                      *
24
# *                                                                         *
25
# ***************************************************************************
26

27
import os
28
import functools
29
import tempfile
30
import threading
31
import json
32
from datetime import date
33
from typing import Dict
34

35
from PySide import QtGui, QtCore, QtWidgets
36
import FreeCAD
37
import FreeCADGui
38

39
from addonmanager_workers_startup import (
40
    CreateAddonListWorker,
41
    LoadPackagesFromCacheWorker,
42
    LoadMacrosFromCacheWorker,
43
    CheckWorkbenchesForUpdatesWorker,
44
    CacheMacroCodeWorker,
45
    GetBasicAddonStatsWorker,
46
    GetAddonScoreWorker,
47
)
48
from addonmanager_workers_installation import (
49
    UpdateMetadataCacheWorker,
50
)
51
from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
52
from addonmanager_uninstaller_gui import AddonUninstallerGUI
53
from addonmanager_update_all_gui import UpdateAllGUI
54
import addonmanager_utilities as utils
55
import addonmanager_freecad_interface as fci
56
import AddonManager_rc  # This is required by Qt, it's not unused
57
from composite_view import CompositeView
58
from Widgets.addonmanager_widget_global_buttons import WidgetGlobalButtonBar
59
from package_list import PackageListItemModel
60
from Addon import Addon
61
from AddonStats import AddonStats
62
from manage_python_dependencies import (
63
    PythonPackageManager,
64
)
65
from addonmanager_cache import local_cache_needs_update
66
from addonmanager_devmode import DeveloperMode
67
from addonmanager_firstrun import FirstRunDialog
68
from addonmanager_connection_checker import ConnectionCheckerGUI
69
from addonmanager_devmode_metadata_checker import MetadataValidators
70

71
import NetworkManager
72

73
from AddonManagerOptions import AddonManagerOptions
74

75
translate = FreeCAD.Qt.translate
76

77

78
def QT_TRANSLATE_NOOP(_, txt):
79
    return txt
80

81

82
__title__ = "FreeCAD Addon Manager Module"
83
__author__ = "Yorik van Havre", "Jonathan Wiedemann", "Kurt Kremitzki", "Chris Hennes"
84
__url__ = "https://www.freecad.org"
85

86
"""
87
FreeCAD Addon Manager Module
88

89
Fetches various types of addons from a variety of sources. Built-in sources are:
90
* https://github.com/FreeCAD/FreeCAD-addons
91
* https://github.com/FreeCAD/FreeCAD-macros
92
* https://wiki.freecad.org/
93

94
Additional git sources may be configure via user preferences.
95

96
You need a working internet connection, and optionally git -- if git is not available, ZIP archives
97
are downloaded instead.
98
"""
99

100
#  \defgroup ADDONMANAGER AddonManager
101
#  \ingroup ADDONMANAGER
102
#  \brief The Addon Manager allows users to install workbenches and macros made by other users
103
#  @{
104

105
INSTANCE = None
106

107

108
class CommandAddonManager:
109
    """The main Addon Manager class and FreeCAD command"""
110

111
    workers = [
112
        "create_addon_list_worker",
113
        "check_worker",
114
        "show_worker",
115
        "showmacro_worker",
116
        "macro_worker",
117
        "update_metadata_cache_worker",
118
        "load_macro_metadata_worker",
119
        "update_all_worker",
120
        "check_for_python_package_updates_worker",
121
        "get_basic_addon_stats_worker",
122
        "get_addon_score_worker",
123
    ]
124

125
    lock = threading.Lock()
126
    restart_required = False
127

128
    def __init__(self):
129
        QT_TRANSLATE_NOOP("QObject", "Addon Manager")
130
        FreeCADGui.addPreferencePage(
131
            AddonManagerOptions,
132
            "Addon Manager",
133
        )
134

135
        self.check_worker = None
136
        self.check_for_python_package_updates_worker = None
137
        self.update_all_worker = None
138
        self.developer_mode = None
139
        self.installer_gui = None
140
        self.composite_view = None
141
        self.button_bar = None
142

143
        self.update_cache = False
144
        self.dialog = None
145
        self.startup_sequence = []
146
        self.packages_with_updates = set()
147

148
        # Set up the connection checker
149
        self.connection_checker = ConnectionCheckerGUI()
150
        self.connection_checker.connection_available.connect(self.launch)
151

152
        # Give other parts of the AM access to the current instance
153
        global INSTANCE
154
        INSTANCE = self
155

156
    def GetResources(self) -> Dict[str, str]:
157
        """FreeCAD-required function: get the core resource information for this Mod."""
158
        return {
159
            "Pixmap": "AddonManager",
160
            "MenuText": QT_TRANSLATE_NOOP("Std_AddonMgr", "&Addon manager"),
161
            "ToolTip": QT_TRANSLATE_NOOP(
162
                "Std_AddonMgr",
163
                "Manage external workbenches, macros, and preference packs",
164
            ),
165
            "Group": "Tools",
166
        }
167

168
    def Activated(self) -> None:
169
        """FreeCAD-required function: called when the command is activated."""
170
        NetworkManager.InitializeNetworkManager()
171
        firstRunDialog = FirstRunDialog()
172
        if not firstRunDialog.exec():
173
            return
174
        self.connection_checker.start()
175

176
    def launch(self) -> None:
177
        """Shows the Addon Manager UI"""
178

179
        # create the dialog
180
        self.dialog = FreeCADGui.PySideUic.loadUi(
181
            os.path.join(os.path.dirname(__file__), "AddonManager.ui")
182
        )
183
        self.dialog.setObjectName("AddonManager_Main_Window")
184
        # self.dialog.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True)
185

186
        # cleanup the leftovers from previous runs
187
        self.macro_repo_dir = FreeCAD.getUserMacroDir(True)
188
        self.packages_with_updates = set()
189
        self.startup_sequence = []
190
        self.cleanup_workers()
191
        self.update_cache = local_cache_needs_update()
192

193
        # restore window geometry from stored state
194
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
195
        w = pref.GetInt("WindowWidth", 800)
196
        h = pref.GetInt("WindowHeight", 600)
197
        self.composite_view = CompositeView(self.dialog)
198
        self.button_bar = WidgetGlobalButtonBar(self.dialog)
199

200
        # If we are checking for updates automatically, hide the Check for updates button:
201
        autocheck = pref.GetBool("AutoCheck", False)
202
        if autocheck:
203
            self.button_bar.check_for_updates.hide()
204
        else:
205
            self.button_bar.update_all_addons.hide()
206

207
        # Set up the listing of packages using the model-view-controller architecture
208
        self.item_model = PackageListItemModel()
209
        self.composite_view.setModel(self.item_model)
210
        self.dialog.layout().addWidget(self.composite_view)
211
        self.dialog.layout().addWidget(self.button_bar)
212

213
        # set nice icons to everything, by theme with fallback to FreeCAD icons
214
        self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
215

216
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
217
        dev_mode_active = pref.GetBool("developerMode", False)
218

219
        # enable/disable stuff
220
        self.button_bar.update_all_addons.setEnabled(False)
221
        self.hide_progress_widgets()
222
        self.button_bar.refresh_local_cache.setEnabled(False)
223
        self.button_bar.refresh_local_cache.setText(translate("AddonsInstaller", "Starting up..."))
224
        if dev_mode_active:
225
            self.button_bar.developer_tools.show()
226
        else:
227
            self.button_bar.developer_tools.hide()
228

229
        # connect slots
230
        self.dialog.rejected.connect(self.reject)
231
        self.button_bar.update_all_addons.clicked.connect(self.update_all)
232
        self.button_bar.close.clicked.connect(self.dialog.reject)
233
        self.button_bar.refresh_local_cache.clicked.connect(self.on_buttonUpdateCache_clicked)
234
        self.button_bar.check_for_updates.clicked.connect(
235
            lambda: self.force_check_updates(standalone=True)
236
        )
237
        self.button_bar.python_dependencies.clicked.connect(self.show_python_updates_dialog)
238
        self.button_bar.developer_tools.clicked.connect(self.show_developer_tools)
239
        self.composite_view.package_list.ui.progressBar.stop_clicked.connect(self.stop_update)
240
        self.composite_view.package_list.setEnabled(False)
241
        self.composite_view.execute.connect(self.executemacro)
242
        self.composite_view.install.connect(self.launch_installer_gui)
243
        self.composite_view.uninstall.connect(self.remove)
244
        self.composite_view.update.connect(self.update)
245
        self.composite_view.update_status.connect(self.status_updated)
246

247
        # center the dialog over the FreeCAD window
248
        self.dialog.resize(w, h)
249
        mw = FreeCADGui.getMainWindow()
250
        self.dialog.move(
251
            mw.frameGeometry().topLeft() + mw.rect().center() - self.dialog.rect().center()
252
        )
253

254
        # begin populating the table in a set of sub-threads
255
        self.startup()
256

257
        # set the label text to start with
258
        self.show_information(translate("AddonsInstaller", "Loading addon information"))
259

260
        # rock 'n roll!!!
261
        self.dialog.exec()
262

263
    def cleanup_workers(self) -> None:
264
        """Ensure that no workers are running by explicitly asking them to stop and waiting for
265
        them until they do"""
266
        for worker in self.workers:
267
            if hasattr(self, worker):
268
                thread = getattr(self, worker)
269
                if thread:
270
                    if not thread.isFinished():
271
                        thread.blockSignals(True)
272
                        thread.requestInterruption()
273
        for worker in self.workers:
274
            if hasattr(self, worker):
275
                thread = getattr(self, worker)
276
                if thread:
277
                    if not thread.isFinished():
278
                        finished = thread.wait(500)
279
                        if not finished:
280
                            FreeCAD.Console.PrintWarning(
281
                                translate(
282
                                    "AddonsInstaller",
283
                                    "Worker process {} is taking a long time to stop...",
284
                                ).format(worker)
285
                                + "\n"
286
                            )
287

288
    def reject(self) -> None:
289
        """called when the window has been closed"""
290

291
        # save window geometry for next use
292
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
293
        pref.SetInt("WindowWidth", self.dialog.width())
294
        pref.SetInt("WindowHeight", self.dialog.height())
295

296
        # ensure all threads are finished before closing
297
        oktoclose = True
298
        worker_killed = False
299
        self.startup_sequence = []
300
        for worker in self.workers:
301
            if hasattr(self, worker):
302
                thread = getattr(self, worker)
303
                if thread:
304
                    if not thread.isFinished():
305
                        thread.blockSignals(True)
306
                        thread.requestInterruption()
307
                        worker_killed = True
308
                        oktoclose = False
309
        while not oktoclose:
310
            oktoclose = True
311
            for worker in self.workers:
312
                if hasattr(self, worker):
313
                    thread = getattr(self, worker)
314
                    if thread:
315
                        thread.wait(25)
316
                        if not thread.isFinished():
317
                            oktoclose = False
318
            QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
319

320
        # Write the cache data if it's safe to do so:
321
        if not worker_killed:
322
            for repo in self.item_model.repos:
323
                if repo.repo_type == Addon.Kind.MACRO:
324
                    self.cache_macro(repo)
325
                else:
326
                    self.cache_package(repo)
327
            self.write_package_cache()
328
            self.write_macro_cache()
329
        else:
330
            self.write_cache_stopfile()
331
            FreeCAD.Console.PrintLog(
332
                "Not writing the cache because a process was forcibly terminated and the state is "
333
                "unknown.\n"
334
            )
335

336
        if self.restart_required:
337
            # display restart dialog
338
            m = QtWidgets.QMessageBox()
339
            m.setWindowTitle(translate("AddonsInstaller", "Addon manager"))
340
            m.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
341
            m.setText(
342
                translate(
343
                    "AddonsInstaller",
344
                    "You must restart FreeCAD for changes to take effect.",
345
                )
346
            )
347
            m.setIcon(m.Warning)
348
            m.setStandardButtons(m.Ok | m.Cancel)
349
            m.setDefaultButton(m.Cancel)
350
            okBtn = m.button(QtWidgets.QMessageBox.StandardButton.Ok)
351
            cancelBtn = m.button(QtWidgets.QMessageBox.StandardButton.Cancel)
352
            okBtn.setText(translate("AddonsInstaller", "Restart now"))
353
            cancelBtn.setText(translate("AddonsInstaller", "Restart later"))
354
            ret = m.exec_()
355
            if ret == m.Ok:
356
                # restart FreeCAD after a delay to give time to this dialog to close
357
                QtCore.QTimer.singleShot(1000, utils.restart_freecad)
358

359
    def startup(self) -> None:
360
        """Downloads the available packages listings and populates the table
361

362
        This proceeds in four stages: first, the main GitHub repository is queried for a list of
363
        possible addons. Each addon is specified as a git submodule with name and branch
364
        information. The actual specific commit ID of the submodule (as listed on Github) is
365
        ignored. Any extra repositories specified by the user are appended to this list.
366

367
        Second, the list of macros is downloaded from the FreeCAD/FreeCAD-macros repository and
368
        the wiki.
369

370
        Third, each of these items is queried for a package.xml metadata file. If that file exists
371
        it is downloaded, cached, and any icons that it references are also downloaded and cached.
372

373
        Finally, for workbenches that are not contained within a package (e.g. they provide no
374
        metadata), an additional git query is made to see if an update is available. Macros are
375
        checked for file changes.
376

377
        Each of these stages is launched in a separate thread to ensure that the UI remains
378
        responsive, and the operation can be cancelled.
379

380
        Each stage is also subject to caching, so may return immediately, if no cache update has
381
        been requested."""
382

383
        # Each function in this list is expected to launch a thread and connect its completion
384
        # signal to self.do_next_startup_phase, or to shortcut to calling
385
        # self.do_next_startup_phase if it is not launching a worker
386
        self.startup_sequence = [
387
            self.populate_packages_table,
388
            self.activate_table_widgets,
389
            self.populate_macros,
390
            self.update_metadata_cache,
391
            self.check_updates,
392
            self.check_python_updates,
393
            self.fetch_addon_stats,
394
            self.fetch_addon_score,
395
            self.select_addon,
396
        ]
397
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
398
        if pref.GetBool("DownloadMacros", False):
399
            self.startup_sequence.append(self.load_macro_metadata)
400
        self.number_of_progress_regions = len(self.startup_sequence)
401
        self.current_progress_region = 0
402
        self.do_next_startup_phase()
403

404
    def do_next_startup_phase(self) -> None:
405
        """Pop the top item in self.startup_sequence off the list and run it"""
406

407
        if len(self.startup_sequence) > 0:
408
            phase_runner = self.startup_sequence.pop(0)
409
            self.current_progress_region += 1
410
            phase_runner()
411
        else:
412
            self.hide_progress_widgets()
413
            self.update_cache = False
414
            self.button_bar.refresh_local_cache.setEnabled(True)
415
            self.button_bar.refresh_local_cache.setText(
416
                translate("AddonsInstaller", "Refresh local cache")
417
            )
418
            pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
419
            pref.SetString("LastCacheUpdate", date.today().isoformat())
420
            self.composite_view.package_list.item_filter.invalidateFilter()
421

422
    def populate_packages_table(self) -> None:
423
        self.item_model.clear()
424

425
        use_cache = not self.update_cache
426
        if use_cache:
427
            if os.path.isfile(utils.get_cache_file_name("package_cache.json")):
428
                with open(utils.get_cache_file_name("package_cache.json"), encoding="utf-8") as f:
429
                    data = f.read()
430
                    try:
431
                        from_json = json.loads(data)
432
                        if len(from_json) == 0:
433
                            use_cache = False
434
                    except json.JSONDecodeError:
435
                        use_cache = False
436
            else:
437
                use_cache = False
438

439
        if not use_cache:
440
            self.update_cache = True  # Make sure to trigger the other cache updates, if the json
441
            # file was missing
442
            self.create_addon_list_worker = CreateAddonListWorker()
443
            self.create_addon_list_worker.status_message.connect(self.show_information)
444
            self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
445
            self.update_progress_bar(10, 100)
446
            self.create_addon_list_worker.finished.connect(
447
                self.do_next_startup_phase
448
            )  # Link to step 2
449
            self.create_addon_list_worker.start()
450
        else:
451
            self.create_addon_list_worker = LoadPackagesFromCacheWorker(
452
                utils.get_cache_file_name("package_cache.json")
453
            )
454
            self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
455
            self.update_progress_bar(10, 100)
456
            self.create_addon_list_worker.finished.connect(
457
                self.do_next_startup_phase
458
            )  # Link to step 2
459
            self.create_addon_list_worker.start()
460

461
    def cache_package(self, repo: Addon):
462
        if not hasattr(self, "package_cache"):
463
            self.package_cache = {}
464
        self.package_cache[repo.name] = repo.to_cache()
465

466
    def write_package_cache(self):
467
        if hasattr(self, "package_cache"):
468
            package_cache_path = utils.get_cache_file_name("package_cache.json")
469
            with open(package_cache_path, "w", encoding="utf-8") as f:
470
                f.write(json.dumps(self.package_cache, indent="  "))
471

472
    def activate_table_widgets(self) -> None:
473
        self.composite_view.package_list.setEnabled(True)
474
        self.composite_view.package_list.ui.view_bar.search.setFocus()
475
        self.do_next_startup_phase()
476

477
    def populate_macros(self) -> None:
478
        macro_cache_file = utils.get_cache_file_name("macro_cache.json")
479
        cache_is_bad = True
480
        if os.path.isfile(macro_cache_file):
481
            size = os.path.getsize(macro_cache_file)
482
            if size > 1000:  # Make sure there is actually data in there
483
                cache_is_bad = False
484
        if cache_is_bad:
485
            if not self.update_cache:
486
                self.update_cache = True  # Make sure to trigger the other cache updates, if the
487
                # json file was missing
488
                self.create_addon_list_worker = CreateAddonListWorker()
489
                self.create_addon_list_worker.status_message.connect(self.show_information)
490
                self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
491
                self.update_progress_bar(10, 100)
492
                self.create_addon_list_worker.finished.connect(
493
                    self.do_next_startup_phase
494
                )  # Link to step 2
495
                self.create_addon_list_worker.start()
496
            else:
497
                # It's already been done in the previous step (TODO: Refactor to eliminate this
498
                # step)
499
                self.do_next_startup_phase()
500
        else:
501
            self.macro_worker = LoadMacrosFromCacheWorker(
502
                utils.get_cache_file_name("macro_cache.json")
503
            )
504
            self.macro_worker.add_macro_signal.connect(self.add_addon_repo)
505
            self.macro_worker.finished.connect(self.do_next_startup_phase)
506
            self.macro_worker.start()
507

508
    def cache_macro(self, repo: Addon):
509
        if not hasattr(self, "macro_cache"):
510
            self.macro_cache = []
511
        if repo.macro is not None:
512
            self.macro_cache.append(repo.macro.to_cache())
513
        else:
514
            FreeCAD.Console.PrintError(
515
                f"Addon Manager: Internal error, cache_macro called on non-macro {repo.name}\n"
516
            )
517

518
    def write_macro_cache(self):
519
        if not hasattr(self, "macro_cache"):
520
            return
521
        macro_cache_path = utils.get_cache_file_name("macro_cache.json")
522
        with open(macro_cache_path, "w", encoding="utf-8") as f:
523
            f.write(json.dumps(self.macro_cache, indent="  "))
524
            self.macro_cache = []
525

526
    def update_metadata_cache(self) -> None:
527
        if self.update_cache:
528
            self.update_metadata_cache_worker = UpdateMetadataCacheWorker(self.item_model.repos)
529
            self.update_metadata_cache_worker.status_message.connect(self.show_information)
530
            self.update_metadata_cache_worker.finished.connect(
531
                self.do_next_startup_phase
532
            )  # Link to step 4
533
            self.update_metadata_cache_worker.progress_made.connect(self.update_progress_bar)
534
            self.update_metadata_cache_worker.package_updated.connect(self.on_package_updated)
535
            self.update_metadata_cache_worker.start()
536
        else:
537
            self.do_next_startup_phase()
538

539
    def on_buttonUpdateCache_clicked(self) -> None:
540
        self.update_cache = True
541
        cache_path = FreeCAD.getUserCachePath()
542
        am_path = os.path.join(cache_path, "AddonManager")
543
        utils.rmdir(am_path)
544
        self.button_bar.refresh_local_cache.setEnabled(False)
545
        self.button_bar.refresh_local_cache.setText(
546
            translate("AddonsInstaller", "Updating cache...")
547
        )
548
        self.startup()
549

550
        # Re-caching implies checking for updates, regardless of the user's autocheck option
551
        if self.check_updates in self.startup_sequence:
552
            self.startup_sequence.remove(self.check_updates)
553
        self.startup_sequence.append(self.force_check_updates)
554

555
    def on_package_updated(self, repo: Addon) -> None:
556
        """Called when the named package has either new metadata or a new icon (or both)"""
557

558
        with self.lock:
559
            repo.icon = self.get_icon(repo, update=True)
560
            self.item_model.reload_item(repo)
561

562
    def load_macro_metadata(self) -> None:
563
        if self.update_cache:
564
            self.load_macro_metadata_worker = CacheMacroCodeWorker(self.item_model.repos)
565
            self.load_macro_metadata_worker.status_message.connect(self.show_information)
566
            self.load_macro_metadata_worker.update_macro.connect(self.on_package_updated)
567
            self.load_macro_metadata_worker.progress_made.connect(self.update_progress_bar)
568
            self.load_macro_metadata_worker.finished.connect(self.do_next_startup_phase)
569
            self.load_macro_metadata_worker.start()
570
        else:
571
            self.do_next_startup_phase()
572

573
    def select_addon(self) -> None:
574
        prefs = fci.Preferences()
575
        selection = prefs.get("SelectedAddon")
576
        if selection:
577
            self.composite_view.package_list.select_addon(selection)
578
            prefs.set("SelectedAddon", "")
579
        self.do_next_startup_phase()
580

581
    def check_updates(self) -> None:
582
        "checks every installed addon for available updates"
583

584
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
585
        autocheck = pref.GetBool("AutoCheck", False)
586
        if not autocheck:
587
            FreeCAD.Console.PrintLog(
588
                "Addon Manager: Skipping update check because AutoCheck user preference is False\n"
589
            )
590
            self.do_next_startup_phase()
591
            return
592
        if not self.packages_with_updates:
593
            self.force_check_updates(standalone=False)
594
        else:
595
            self.do_next_startup_phase()
596

597
    def force_check_updates(self, standalone=False) -> None:
598
        if hasattr(self, "check_worker"):
599
            thread = self.check_worker
600
            if thread:
601
                if not thread.isFinished():
602
                    self.do_next_startup_phase()
603
                    return
604

605
        self.button_bar.update_all_addons.setText(
606
            translate("AddonsInstaller", "Checking for updates...")
607
        )
608
        self.packages_with_updates.clear()
609
        self.button_bar.update_all_addons.show()
610
        self.button_bar.check_for_updates.setDisabled(True)
611
        self.check_worker = CheckWorkbenchesForUpdatesWorker(self.item_model.repos)
612
        self.check_worker.finished.connect(self.do_next_startup_phase)
613
        self.check_worker.finished.connect(self.update_check_complete)
614
        self.check_worker.progress_made.connect(self.update_progress_bar)
615
        if standalone:
616
            self.current_progress_region = 1
617
            self.number_of_progress_regions = 1
618
        self.check_worker.update_status.connect(self.status_updated)
619
        self.check_worker.start()
620
        self.enable_updates(len(self.packages_with_updates))
621

622
    def status_updated(self, repo: Addon) -> None:
623
        self.item_model.reload_item(repo)
624
        if repo.status() == Addon.Status.UPDATE_AVAILABLE:
625
            self.packages_with_updates.add(repo)
626
            self.enable_updates(len(self.packages_with_updates))
627
        elif repo.status() == Addon.Status.PENDING_RESTART:
628
            self.restart_required = True
629

630
    def enable_updates(self, number_of_updates: int) -> None:
631
        """enables the update button"""
632

633
        if number_of_updates:
634
            self.button_bar.set_number_of_available_updates(number_of_updates)
635
        elif (
636
            hasattr(self, "check_worker")
637
            and self.check_worker is not None
638
            and self.check_worker.isRunning()
639
        ):
640
            self.button_bar.update_all_addons.setText(
641
                translate("AddonsInstaller", "Checking for updates...")
642
            )
643
        else:
644
            self.button_bar.set_number_of_available_updates(0)
645

646
    def update_check_complete(self) -> None:
647
        self.enable_updates(len(self.packages_with_updates))
648
        self.button_bar.check_for_updates.setEnabled(True)
649

650
    def check_python_updates(self) -> None:
651
        PythonPackageManager.migrate_old_am_installations()  # Migrate 0.20 to 0.21
652
        self.do_next_startup_phase()
653

654
    def show_python_updates_dialog(self) -> None:
655
        if not hasattr(self, "manage_python_packages_dialog"):
656
            self.manage_python_packages_dialog = PythonPackageManager(self.item_model.repos)
657
        self.manage_python_packages_dialog.show()
658

659
    def fetch_addon_stats(self) -> None:
660
        """Fetch the Addon Stats JSON data from a URL"""
661
        pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
662
        url = pref.GetString("AddonsStatsURL", "https://freecad.org/addon_stats.json")
663
        if url and url != "NONE":
664
            self.get_basic_addon_stats_worker = GetBasicAddonStatsWorker(
665
                url, self.item_model.repos, self.dialog
666
            )
667
            self.get_basic_addon_stats_worker.finished.connect(self.do_next_startup_phase)
668
            self.get_basic_addon_stats_worker.update_addon_stats.connect(self.update_addon_stats)
669
            self.get_basic_addon_stats_worker.start()
670
        else:
671
            self.do_next_startup_phase()
672

673
    def update_addon_stats(self, addon: Addon):
674
        self.item_model.reload_item(addon)
675

676
    def fetch_addon_score(self) -> None:
677
        """Fetch the Addon score JSON data from a URL"""
678
        prefs = fci.Preferences()
679
        url = prefs.get("AddonsScoreURL")
680
        if url and url != "NONE":
681
            self.get_addon_score_worker = GetAddonScoreWorker(
682
                url, self.item_model.repos, self.dialog
683
            )
684
            self.get_addon_score_worker.finished.connect(self.score_fetched_successfully)
685
            self.get_addon_score_worker.finished.connect(self.do_next_startup_phase)
686
            self.get_addon_score_worker.update_addon_score.connect(self.update_addon_score)
687
            self.get_addon_score_worker.start()
688
        else:
689
            self.composite_view.package_list.ui.view_bar.set_rankings_available(False)
690
            self.do_next_startup_phase()
691

692
    def update_addon_score(self, addon: Addon):
693
        self.item_model.reload_item(addon)
694

695
    def score_fetched_successfully(self):
696
        self.composite_view.package_list.ui.view_bar.set_rankings_available(True)
697

698
    def show_developer_tools(self) -> None:
699
        """Display the developer tools dialog"""
700
        if not self.developer_mode:
701
            self.developer_mode = DeveloperMode()
702
        self.developer_mode.show()
703

704
        checker = MetadataValidators()
705
        checker.validate_all(self.item_model.repos)
706

707
    def add_addon_repo(self, addon_repo: Addon) -> None:
708
        """adds a workbench to the list"""
709

710
        if addon_repo.icon is None or addon_repo.icon.isNull():
711
            addon_repo.icon = self.get_icon(addon_repo)
712
        for repo in self.item_model.repos:
713
            if repo.name == addon_repo.name:
714
                # self.item_model.reload_item(repo) # If we want to have later additions superseded
715
                # earlier
716
                return
717
        self.item_model.append_item(addon_repo)
718

719
    def get_icon(self, repo: Addon, update: bool = False) -> QtGui.QIcon:
720
        """Returns an icon for an Addon. Uses a cached icon if possible, unless update is True,
721
        in which case the icon is regenerated."""
722

723
        if not update and repo.icon and not repo.icon.isNull() and repo.icon.isValid():
724
            return repo.icon
725

726
        path = ":/icons/" + repo.name.replace(" ", "_")
727
        if repo.repo_type == Addon.Kind.WORKBENCH:
728
            path += "_workbench_icon.svg"
729
            default_icon = QtGui.QIcon(":/icons/document-package.svg")
730
        elif repo.repo_type == Addon.Kind.MACRO:
731
            if repo.macro and repo.macro.icon:
732
                if os.path.isabs(repo.macro.icon):
733
                    path = repo.macro.icon
734
                    default_icon = QtGui.QIcon(":/icons/document-python.svg")
735
                else:
736
                    path = os.path.join(os.path.dirname(repo.macro.src_filename), repo.macro.icon)
737
                    default_icon = QtGui.QIcon(":/icons/document-python.svg")
738
            elif repo.macro and repo.macro.xpm:
739
                cache_path = FreeCAD.getUserCachePath()
740
                am_path = os.path.join(cache_path, "AddonManager", "MacroIcons")
741
                os.makedirs(am_path, exist_ok=True)
742
                path = os.path.join(am_path, repo.name + "_icon.xpm")
743
                if not os.path.exists(path):
744
                    with open(path, "w") as f:
745
                        f.write(repo.macro.xpm)
746
                default_icon = QtGui.QIcon(repo.macro.xpm)
747
            else:
748
                path += "_macro_icon.svg"
749
                default_icon = QtGui.QIcon(":/icons/document-python.svg")
750
        elif repo.repo_type == Addon.Kind.PACKAGE:
751
            # The cache might not have been downloaded yet, check to see if it's there...
752
            if os.path.isfile(repo.get_cached_icon_filename()):
753
                path = repo.get_cached_icon_filename()
754
            elif repo.contains_workbench():
755
                path += "_workbench_icon.svg"
756
                default_icon = QtGui.QIcon(":/icons/document-package.svg")
757
            elif repo.contains_macro():
758
                path += "_macro_icon.svg"
759
                default_icon = QtGui.QIcon(":/icons/document-python.svg")
760
            else:
761
                default_icon = QtGui.QIcon(":/icons/document-package.svg")
762

763
        if QtCore.QFile.exists(path):
764
            addonicon = QtGui.QIcon(path)
765
        else:
766
            addonicon = default_icon
767
        repo.icon = addonicon
768

769
        return addonicon
770

771
    def show_information(self, message: str) -> None:
772
        """shows generic text in the information pane"""
773

774
        self.composite_view.package_list.ui.progressBar.set_status(message)
775
        self.composite_view.package_list.ui.progressBar.repaint()
776

777
    def append_to_repos_list(self, repo: Addon) -> None:
778
        """this function allows threads to update the main list of workbenches"""
779
        self.item_model.append_item(repo)
780

781
    def update(self, repo: Addon) -> None:
782
        self.launch_installer_gui(repo)
783

784
    def mark_repo_update_available(self, repo: Addon, available: bool) -> None:
785
        if available:
786
            repo.set_status(Addon.Status.UPDATE_AVAILABLE)
787
        else:
788
            repo.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
789
        self.item_model.reload_item(repo)
790
        self.composite_view.package_details_controller.show_repo(repo)
791

792
    def launch_installer_gui(self, addon: Addon) -> None:
793
        if self.installer_gui is not None:
794
            FreeCAD.Console.PrintError(
795
                translate(
796
                    "AddonsInstaller",
797
                    "Cannot launch a new installer until the previous one has finished.",
798
                )
799
            )
800
            return
801
        if addon.macro is not None:
802
            self.installer_gui = MacroInstallerGUI(addon)
803
        else:
804
            self.installer_gui = AddonInstallerGUI(addon, self.item_model.repos)
805
        self.installer_gui.success.connect(self.on_package_status_changed)
806
        self.installer_gui.finished.connect(self.cleanup_installer)
807
        self.installer_gui.run()  # Does not block
808

809
    def cleanup_installer(self) -> None:
810
        QtCore.QTimer.singleShot(500, self.no_really_clean_up_the_installer)
811

812
    def no_really_clean_up_the_installer(self) -> None:
813
        self.installer_gui = None
814

815
    def update_all(self) -> None:
816
        """Asynchronously apply all available updates: individual failures are noted, but do not
817
        stop other updates"""
818

819
        if self.installer_gui is not None:
820
            FreeCAD.Console.PrintError(
821
                translate(
822
                    "AddonsInstaller",
823
                    "Cannot launch a new installer until the previous one has finished.",
824
                )
825
            )
826
            return
827

828
        self.installer_gui = UpdateAllGUI(self.item_model.repos)
829
        self.installer_gui.addon_updated.connect(self.on_package_status_changed)
830
        self.installer_gui.finished.connect(self.cleanup_installer)
831
        self.installer_gui.run()  # Does not block
832

833
    def hide_progress_widgets(self) -> None:
834
        """hides the progress bar and related widgets"""
835

836
        self.composite_view.package_list.ui.progressBar.hide()
837
        self.composite_view.package_list.ui.view_bar.search.setFocus()
838

839
    def show_progress_widgets(self) -> None:
840
        if self.composite_view.package_list.ui.progressBar.isHidden():
841
            self.composite_view.package_list.ui.progressBar.show()
842

843
    def update_progress_bar(self, current_value: int, max_value: int) -> None:
844
        """Update the progress bar, showing it if it's hidden"""
845

846
        max_value = max_value if max_value > 0 else 1
847

848
        if current_value < 0:
849
            current_value = 0
850
        elif current_value > max_value:
851
            current_value = max_value
852

853
        self.show_progress_widgets()
854
        region_size = 100.0 / self.number_of_progress_regions
855
        completed_region_portion = (self.current_progress_region - 1) * region_size
856
        current_region_portion = (float(current_value) / float(max_value)) * region_size
857
        value = completed_region_portion + current_region_portion
858
        self.composite_view.package_list.ui.progressBar.set_value(
859
            value * 10
860
        )  # Out of 1000 segments, so it moves sort of smoothly
861
        self.composite_view.package_list.ui.progressBar.repaint()
862

863
    def stop_update(self) -> None:
864
        self.cleanup_workers()
865
        self.hide_progress_widgets()
866
        self.write_cache_stopfile()
867
        self.button_bar.refresh_local_cache.setEnabled(True)
868
        self.button_bar.refresh_local_cache.setText(
869
            translate("AddonsInstaller", "Refresh local cache")
870
        )
871

872
    def write_cache_stopfile(self) -> None:
873
        stopfile = utils.get_cache_file_name("CACHE_UPDATE_INTERRUPTED")
874
        with open(stopfile, "w", encoding="utf8") as f:
875
            f.write(
876
                "This file indicates that a cache operation was interrupted, and "
877
                "the cache is in an unknown state. It will be deleted next time "
878
                "AddonManager recaches."
879
            )
880

881
    def on_package_status_changed(self, repo: Addon) -> None:
882
        if repo.status() == Addon.Status.PENDING_RESTART:
883
            self.restart_required = True
884
        self.item_model.reload_item(repo)
885
        self.composite_view.package_details_controller.show_repo(repo)
886
        if repo in self.packages_with_updates:
887
            self.packages_with_updates.remove(repo)
888
            self.enable_updates(len(self.packages_with_updates))
889

890
    def executemacro(self, repo: Addon) -> None:
891
        """executes a selected macro"""
892

893
        macro = repo.macro
894
        if not macro or not macro.code:
895
            return
896

897
        if macro.is_installed():
898
            macro_path = os.path.join(self.macro_repo_dir, macro.filename)
899
            FreeCADGui.open(str(macro_path))
900
            self.dialog.hide()
901
            FreeCADGui.SendMsgToActiveView("Run")
902
        else:
903
            with tempfile.TemporaryDirectory() as dir:
904
                temp_install_succeeded = macro.install(dir)
905
                if not temp_install_succeeded:
906
                    message = translate(
907
                        "AddonsInstaller",
908
                        "Execution of macro failed. See console for failure details.",
909
                    )
910
                    return
911
                macro_path = os.path.join(dir, macro.filename)
912
                FreeCADGui.open(str(macro_path))
913
                self.dialog.hide()
914
                FreeCADGui.SendMsgToActiveView("Run")
915

916
    def remove(self, addon: Addon) -> None:
917
        """Remove this addon."""
918
        if self.installer_gui is not None:
919
            FreeCAD.Console.PrintError(
920
                translate(
921
                    "AddonsInstaller",
922
                    "Cannot launch a new installer until the previous one has finished.",
923
                )
924
            )
925
            return
926
        self.installer_gui = AddonUninstallerGUI(addon)
927
        self.installer_gui.finished.connect(self.cleanup_installer)
928
        self.installer_gui.finished.connect(
929
            functools.partial(self.on_package_status_changed, addon)
930
        )
931
        self.installer_gui.run()  # Does not block
932

933

934
# @}
935

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

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

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

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