FreeCAD
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
27import os
28import functools
29import tempfile
30import threading
31import json
32from datetime import date
33from typing import Dict
34
35from PySide import QtGui, QtCore, QtWidgets
36import FreeCAD
37import FreeCADGui
38
39from addonmanager_workers_startup import (
40CreateAddonListWorker,
41LoadPackagesFromCacheWorker,
42LoadMacrosFromCacheWorker,
43CheckWorkbenchesForUpdatesWorker,
44CacheMacroCodeWorker,
45GetBasicAddonStatsWorker,
46GetAddonScoreWorker,
47)
48from addonmanager_workers_installation import (
49UpdateMetadataCacheWorker,
50)
51from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
52from addonmanager_uninstaller_gui import AddonUninstallerGUI
53from addonmanager_update_all_gui import UpdateAllGUI
54import addonmanager_utilities as utils
55import addonmanager_freecad_interface as fci
56import AddonManager_rc # This is required by Qt, it's not unused
57from composite_view import CompositeView
58from Widgets.addonmanager_widget_global_buttons import WidgetGlobalButtonBar
59from package_list import PackageListItemModel
60from Addon import Addon
61from AddonStats import AddonStats
62from manage_python_dependencies import (
63PythonPackageManager,
64)
65from addonmanager_cache import local_cache_needs_update
66from addonmanager_devmode import DeveloperMode
67from addonmanager_firstrun import FirstRunDialog
68from addonmanager_connection_checker import ConnectionCheckerGUI
69from addonmanager_devmode_metadata_checker import MetadataValidators
70
71import NetworkManager
72
73from AddonManagerOptions import AddonManagerOptions
74
75translate = FreeCAD.Qt.translate
76
77
78def QT_TRANSLATE_NOOP(_, txt):
79return 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"""
87FreeCAD Addon Manager Module
88
89Fetches 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
94Additional git sources may be configure via user preferences.
95
96You need a working internet connection, and optionally git -- if git is not available, ZIP archives
97are 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
105INSTANCE = None
106
107
108class CommandAddonManager:
109"""The main Addon Manager class and FreeCAD command"""
110
111workers = [
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
125lock = threading.Lock()
126restart_required = False
127
128def __init__(self):
129QT_TRANSLATE_NOOP("QObject", "Addon Manager")
130FreeCADGui.addPreferencePage(
131AddonManagerOptions,
132"Addon Manager",
133)
134
135self.check_worker = None
136self.check_for_python_package_updates_worker = None
137self.update_all_worker = None
138self.developer_mode = None
139self.installer_gui = None
140self.composite_view = None
141self.button_bar = None
142
143self.update_cache = False
144self.dialog = None
145self.startup_sequence = []
146self.packages_with_updates = set()
147
148# Set up the connection checker
149self.connection_checker = ConnectionCheckerGUI()
150self.connection_checker.connection_available.connect(self.launch)
151
152# Give other parts of the AM access to the current instance
153global INSTANCE
154INSTANCE = self
155
156def GetResources(self) -> Dict[str, str]:
157"""FreeCAD-required function: get the core resource information for this Mod."""
158return {
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
168def Activated(self) -> None:
169"""FreeCAD-required function: called when the command is activated."""
170NetworkManager.InitializeNetworkManager()
171firstRunDialog = FirstRunDialog()
172if not firstRunDialog.exec():
173return
174self.connection_checker.start()
175
176def launch(self) -> None:
177"""Shows the Addon Manager UI"""
178
179# create the dialog
180self.dialog = FreeCADGui.PySideUic.loadUi(
181os.path.join(os.path.dirname(__file__), "AddonManager.ui")
182)
183self.dialog.setObjectName("AddonManager_Main_Window")
184# self.dialog.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True)
185
186# cleanup the leftovers from previous runs
187self.macro_repo_dir = FreeCAD.getUserMacroDir(True)
188self.packages_with_updates = set()
189self.startup_sequence = []
190self.cleanup_workers()
191self.update_cache = local_cache_needs_update()
192
193# restore window geometry from stored state
194pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
195w = pref.GetInt("WindowWidth", 800)
196h = pref.GetInt("WindowHeight", 600)
197self.composite_view = CompositeView(self.dialog)
198self.button_bar = WidgetGlobalButtonBar(self.dialog)
199
200# If we are checking for updates automatically, hide the Check for updates button:
201autocheck = pref.GetBool("AutoCheck", False)
202if autocheck:
203self.button_bar.check_for_updates.hide()
204else:
205self.button_bar.update_all_addons.hide()
206
207# Set up the listing of packages using the model-view-controller architecture
208self.item_model = PackageListItemModel()
209self.composite_view.setModel(self.item_model)
210self.dialog.layout().addWidget(self.composite_view)
211self.dialog.layout().addWidget(self.button_bar)
212
213# set nice icons to everything, by theme with fallback to FreeCAD icons
214self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
215
216pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
217dev_mode_active = pref.GetBool("developerMode", False)
218
219# enable/disable stuff
220self.button_bar.update_all_addons.setEnabled(False)
221self.hide_progress_widgets()
222self.button_bar.refresh_local_cache.setEnabled(False)
223self.button_bar.refresh_local_cache.setText(translate("AddonsInstaller", "Starting up..."))
224if dev_mode_active:
225self.button_bar.developer_tools.show()
226else:
227self.button_bar.developer_tools.hide()
228
229# connect slots
230self.dialog.rejected.connect(self.reject)
231self.button_bar.update_all_addons.clicked.connect(self.update_all)
232self.button_bar.close.clicked.connect(self.dialog.reject)
233self.button_bar.refresh_local_cache.clicked.connect(self.on_buttonUpdateCache_clicked)
234self.button_bar.check_for_updates.clicked.connect(
235lambda: self.force_check_updates(standalone=True)
236)
237self.button_bar.python_dependencies.clicked.connect(self.show_python_updates_dialog)
238self.button_bar.developer_tools.clicked.connect(self.show_developer_tools)
239self.composite_view.package_list.ui.progressBar.stop_clicked.connect(self.stop_update)
240self.composite_view.package_list.setEnabled(False)
241self.composite_view.execute.connect(self.executemacro)
242self.composite_view.install.connect(self.launch_installer_gui)
243self.composite_view.uninstall.connect(self.remove)
244self.composite_view.update.connect(self.update)
245self.composite_view.update_status.connect(self.status_updated)
246
247# center the dialog over the FreeCAD window
248self.dialog.resize(w, h)
249mw = FreeCADGui.getMainWindow()
250self.dialog.move(
251mw.frameGeometry().topLeft() + mw.rect().center() - self.dialog.rect().center()
252)
253
254# begin populating the table in a set of sub-threads
255self.startup()
256
257# set the label text to start with
258self.show_information(translate("AddonsInstaller", "Loading addon information"))
259
260# rock 'n roll!!!
261self.dialog.exec()
262
263def cleanup_workers(self) -> None:
264"""Ensure that no workers are running by explicitly asking them to stop and waiting for
265them until they do"""
266for worker in self.workers:
267if hasattr(self, worker):
268thread = getattr(self, worker)
269if thread:
270if not thread.isFinished():
271thread.blockSignals(True)
272thread.requestInterruption()
273for worker in self.workers:
274if hasattr(self, worker):
275thread = getattr(self, worker)
276if thread:
277if not thread.isFinished():
278finished = thread.wait(500)
279if not finished:
280FreeCAD.Console.PrintWarning(
281translate(
282"AddonsInstaller",
283"Worker process {} is taking a long time to stop...",
284).format(worker)
285+ "\n"
286)
287
288def reject(self) -> None:
289"""called when the window has been closed"""
290
291# save window geometry for next use
292pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
293pref.SetInt("WindowWidth", self.dialog.width())
294pref.SetInt("WindowHeight", self.dialog.height())
295
296# ensure all threads are finished before closing
297oktoclose = True
298worker_killed = False
299self.startup_sequence = []
300for worker in self.workers:
301if hasattr(self, worker):
302thread = getattr(self, worker)
303if thread:
304if not thread.isFinished():
305thread.blockSignals(True)
306thread.requestInterruption()
307worker_killed = True
308oktoclose = False
309while not oktoclose:
310oktoclose = True
311for worker in self.workers:
312if hasattr(self, worker):
313thread = getattr(self, worker)
314if thread:
315thread.wait(25)
316if not thread.isFinished():
317oktoclose = False
318QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
319
320# Write the cache data if it's safe to do so:
321if not worker_killed:
322for repo in self.item_model.repos:
323if repo.repo_type == Addon.Kind.MACRO:
324self.cache_macro(repo)
325else:
326self.cache_package(repo)
327self.write_package_cache()
328self.write_macro_cache()
329else:
330self.write_cache_stopfile()
331FreeCAD.Console.PrintLog(
332"Not writing the cache because a process was forcibly terminated and the state is "
333"unknown.\n"
334)
335
336if self.restart_required:
337# display restart dialog
338m = QtWidgets.QMessageBox()
339m.setWindowTitle(translate("AddonsInstaller", "Addon manager"))
340m.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
341m.setText(
342translate(
343"AddonsInstaller",
344"You must restart FreeCAD for changes to take effect.",
345)
346)
347m.setIcon(m.Warning)
348m.setStandardButtons(m.Ok | m.Cancel)
349m.setDefaultButton(m.Cancel)
350okBtn = m.button(QtWidgets.QMessageBox.StandardButton.Ok)
351cancelBtn = m.button(QtWidgets.QMessageBox.StandardButton.Cancel)
352okBtn.setText(translate("AddonsInstaller", "Restart now"))
353cancelBtn.setText(translate("AddonsInstaller", "Restart later"))
354ret = m.exec_()
355if ret == m.Ok:
356# restart FreeCAD after a delay to give time to this dialog to close
357QtCore.QTimer.singleShot(1000, utils.restart_freecad)
358
359def startup(self) -> None:
360"""Downloads the available packages listings and populates the table
361
362This proceeds in four stages: first, the main GitHub repository is queried for a list of
363possible addons. Each addon is specified as a git submodule with name and branch
364information. The actual specific commit ID of the submodule (as listed on Github) is
365ignored. Any extra repositories specified by the user are appended to this list.
366
367Second, the list of macros is downloaded from the FreeCAD/FreeCAD-macros repository and
368the wiki.
369
370Third, each of these items is queried for a package.xml metadata file. If that file exists
371it is downloaded, cached, and any icons that it references are also downloaded and cached.
372
373Finally, for workbenches that are not contained within a package (e.g. they provide no
374metadata), an additional git query is made to see if an update is available. Macros are
375checked for file changes.
376
377Each of these stages is launched in a separate thread to ensure that the UI remains
378responsive, and the operation can be cancelled.
379
380Each stage is also subject to caching, so may return immediately, if no cache update has
381been 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
386self.startup_sequence = [
387self.populate_packages_table,
388self.activate_table_widgets,
389self.populate_macros,
390self.update_metadata_cache,
391self.check_updates,
392self.check_python_updates,
393self.fetch_addon_stats,
394self.fetch_addon_score,
395self.select_addon,
396]
397pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
398if pref.GetBool("DownloadMacros", False):
399self.startup_sequence.append(self.load_macro_metadata)
400self.number_of_progress_regions = len(self.startup_sequence)
401self.current_progress_region = 0
402self.do_next_startup_phase()
403
404def do_next_startup_phase(self) -> None:
405"""Pop the top item in self.startup_sequence off the list and run it"""
406
407if len(self.startup_sequence) > 0:
408phase_runner = self.startup_sequence.pop(0)
409self.current_progress_region += 1
410phase_runner()
411else:
412self.hide_progress_widgets()
413self.update_cache = False
414self.button_bar.refresh_local_cache.setEnabled(True)
415self.button_bar.refresh_local_cache.setText(
416translate("AddonsInstaller", "Refresh local cache")
417)
418pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
419pref.SetString("LastCacheUpdate", date.today().isoformat())
420self.composite_view.package_list.item_filter.invalidateFilter()
421
422def populate_packages_table(self) -> None:
423self.item_model.clear()
424
425use_cache = not self.update_cache
426if use_cache:
427if os.path.isfile(utils.get_cache_file_name("package_cache.json")):
428with open(utils.get_cache_file_name("package_cache.json"), encoding="utf-8") as f:
429data = f.read()
430try:
431from_json = json.loads(data)
432if len(from_json) == 0:
433use_cache = False
434except json.JSONDecodeError:
435use_cache = False
436else:
437use_cache = False
438
439if not use_cache:
440self.update_cache = True # Make sure to trigger the other cache updates, if the json
441# file was missing
442self.create_addon_list_worker = CreateAddonListWorker()
443self.create_addon_list_worker.status_message.connect(self.show_information)
444self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
445self.update_progress_bar(10, 100)
446self.create_addon_list_worker.finished.connect(
447self.do_next_startup_phase
448) # Link to step 2
449self.create_addon_list_worker.start()
450else:
451self.create_addon_list_worker = LoadPackagesFromCacheWorker(
452utils.get_cache_file_name("package_cache.json")
453)
454self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
455self.update_progress_bar(10, 100)
456self.create_addon_list_worker.finished.connect(
457self.do_next_startup_phase
458) # Link to step 2
459self.create_addon_list_worker.start()
460
461def cache_package(self, repo: Addon):
462if not hasattr(self, "package_cache"):
463self.package_cache = {}
464self.package_cache[repo.name] = repo.to_cache()
465
466def write_package_cache(self):
467if hasattr(self, "package_cache"):
468package_cache_path = utils.get_cache_file_name("package_cache.json")
469with open(package_cache_path, "w", encoding="utf-8") as f:
470f.write(json.dumps(self.package_cache, indent=" "))
471
472def activate_table_widgets(self) -> None:
473self.composite_view.package_list.setEnabled(True)
474self.composite_view.package_list.ui.view_bar.search.setFocus()
475self.do_next_startup_phase()
476
477def populate_macros(self) -> None:
478macro_cache_file = utils.get_cache_file_name("macro_cache.json")
479cache_is_bad = True
480if os.path.isfile(macro_cache_file):
481size = os.path.getsize(macro_cache_file)
482if size > 1000: # Make sure there is actually data in there
483cache_is_bad = False
484if cache_is_bad:
485if not self.update_cache:
486self.update_cache = True # Make sure to trigger the other cache updates, if the
487# json file was missing
488self.create_addon_list_worker = CreateAddonListWorker()
489self.create_addon_list_worker.status_message.connect(self.show_information)
490self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo)
491self.update_progress_bar(10, 100)
492self.create_addon_list_worker.finished.connect(
493self.do_next_startup_phase
494) # Link to step 2
495self.create_addon_list_worker.start()
496else:
497# It's already been done in the previous step (TODO: Refactor to eliminate this
498# step)
499self.do_next_startup_phase()
500else:
501self.macro_worker = LoadMacrosFromCacheWorker(
502utils.get_cache_file_name("macro_cache.json")
503)
504self.macro_worker.add_macro_signal.connect(self.add_addon_repo)
505self.macro_worker.finished.connect(self.do_next_startup_phase)
506self.macro_worker.start()
507
508def cache_macro(self, repo: Addon):
509if not hasattr(self, "macro_cache"):
510self.macro_cache = []
511if repo.macro is not None:
512self.macro_cache.append(repo.macro.to_cache())
513else:
514FreeCAD.Console.PrintError(
515f"Addon Manager: Internal error, cache_macro called on non-macro {repo.name}\n"
516)
517
518def write_macro_cache(self):
519if not hasattr(self, "macro_cache"):
520return
521macro_cache_path = utils.get_cache_file_name("macro_cache.json")
522with open(macro_cache_path, "w", encoding="utf-8") as f:
523f.write(json.dumps(self.macro_cache, indent=" "))
524self.macro_cache = []
525
526def update_metadata_cache(self) -> None:
527if self.update_cache:
528self.update_metadata_cache_worker = UpdateMetadataCacheWorker(self.item_model.repos)
529self.update_metadata_cache_worker.status_message.connect(self.show_information)
530self.update_metadata_cache_worker.finished.connect(
531self.do_next_startup_phase
532) # Link to step 4
533self.update_metadata_cache_worker.progress_made.connect(self.update_progress_bar)
534self.update_metadata_cache_worker.package_updated.connect(self.on_package_updated)
535self.update_metadata_cache_worker.start()
536else:
537self.do_next_startup_phase()
538
539def on_buttonUpdateCache_clicked(self) -> None:
540self.update_cache = True
541cache_path = FreeCAD.getUserCachePath()
542am_path = os.path.join(cache_path, "AddonManager")
543utils.rmdir(am_path)
544self.button_bar.refresh_local_cache.setEnabled(False)
545self.button_bar.refresh_local_cache.setText(
546translate("AddonsInstaller", "Updating cache...")
547)
548self.startup()
549
550# Re-caching implies checking for updates, regardless of the user's autocheck option
551if self.check_updates in self.startup_sequence:
552self.startup_sequence.remove(self.check_updates)
553self.startup_sequence.append(self.force_check_updates)
554
555def on_package_updated(self, repo: Addon) -> None:
556"""Called when the named package has either new metadata or a new icon (or both)"""
557
558with self.lock:
559repo.icon = self.get_icon(repo, update=True)
560self.item_model.reload_item(repo)
561
562def load_macro_metadata(self) -> None:
563if self.update_cache:
564self.load_macro_metadata_worker = CacheMacroCodeWorker(self.item_model.repos)
565self.load_macro_metadata_worker.status_message.connect(self.show_information)
566self.load_macro_metadata_worker.update_macro.connect(self.on_package_updated)
567self.load_macro_metadata_worker.progress_made.connect(self.update_progress_bar)
568self.load_macro_metadata_worker.finished.connect(self.do_next_startup_phase)
569self.load_macro_metadata_worker.start()
570else:
571self.do_next_startup_phase()
572
573def select_addon(self) -> None:
574prefs = fci.Preferences()
575selection = prefs.get("SelectedAddon")
576if selection:
577self.composite_view.package_list.select_addon(selection)
578prefs.set("SelectedAddon", "")
579self.do_next_startup_phase()
580
581def check_updates(self) -> None:
582"checks every installed addon for available updates"
583
584pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
585autocheck = pref.GetBool("AutoCheck", False)
586if not autocheck:
587FreeCAD.Console.PrintLog(
588"Addon Manager: Skipping update check because AutoCheck user preference is False\n"
589)
590self.do_next_startup_phase()
591return
592if not self.packages_with_updates:
593self.force_check_updates(standalone=False)
594else:
595self.do_next_startup_phase()
596
597def force_check_updates(self, standalone=False) -> None:
598if hasattr(self, "check_worker"):
599thread = self.check_worker
600if thread:
601if not thread.isFinished():
602self.do_next_startup_phase()
603return
604
605self.button_bar.update_all_addons.setText(
606translate("AddonsInstaller", "Checking for updates...")
607)
608self.packages_with_updates.clear()
609self.button_bar.update_all_addons.show()
610self.button_bar.check_for_updates.setDisabled(True)
611self.check_worker = CheckWorkbenchesForUpdatesWorker(self.item_model.repos)
612self.check_worker.finished.connect(self.do_next_startup_phase)
613self.check_worker.finished.connect(self.update_check_complete)
614self.check_worker.progress_made.connect(self.update_progress_bar)
615if standalone:
616self.current_progress_region = 1
617self.number_of_progress_regions = 1
618self.check_worker.update_status.connect(self.status_updated)
619self.check_worker.start()
620self.enable_updates(len(self.packages_with_updates))
621
622def status_updated(self, repo: Addon) -> None:
623self.item_model.reload_item(repo)
624if repo.status() == Addon.Status.UPDATE_AVAILABLE:
625self.packages_with_updates.add(repo)
626self.enable_updates(len(self.packages_with_updates))
627elif repo.status() == Addon.Status.PENDING_RESTART:
628self.restart_required = True
629
630def enable_updates(self, number_of_updates: int) -> None:
631"""enables the update button"""
632
633if number_of_updates:
634self.button_bar.set_number_of_available_updates(number_of_updates)
635elif (
636hasattr(self, "check_worker")
637and self.check_worker is not None
638and self.check_worker.isRunning()
639):
640self.button_bar.update_all_addons.setText(
641translate("AddonsInstaller", "Checking for updates...")
642)
643else:
644self.button_bar.set_number_of_available_updates(0)
645
646def update_check_complete(self) -> None:
647self.enable_updates(len(self.packages_with_updates))
648self.button_bar.check_for_updates.setEnabled(True)
649
650def check_python_updates(self) -> None:
651PythonPackageManager.migrate_old_am_installations() # Migrate 0.20 to 0.21
652self.do_next_startup_phase()
653
654def show_python_updates_dialog(self) -> None:
655if not hasattr(self, "manage_python_packages_dialog"):
656self.manage_python_packages_dialog = PythonPackageManager(self.item_model.repos)
657self.manage_python_packages_dialog.show()
658
659def fetch_addon_stats(self) -> None:
660"""Fetch the Addon Stats JSON data from a URL"""
661pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
662url = pref.GetString("AddonsStatsURL", "https://freecad.org/addon_stats.json")
663if url and url != "NONE":
664self.get_basic_addon_stats_worker = GetBasicAddonStatsWorker(
665url, self.item_model.repos, self.dialog
666)
667self.get_basic_addon_stats_worker.finished.connect(self.do_next_startup_phase)
668self.get_basic_addon_stats_worker.update_addon_stats.connect(self.update_addon_stats)
669self.get_basic_addon_stats_worker.start()
670else:
671self.do_next_startup_phase()
672
673def update_addon_stats(self, addon: Addon):
674self.item_model.reload_item(addon)
675
676def fetch_addon_score(self) -> None:
677"""Fetch the Addon score JSON data from a URL"""
678prefs = fci.Preferences()
679url = prefs.get("AddonsScoreURL")
680if url and url != "NONE":
681self.get_addon_score_worker = GetAddonScoreWorker(
682url, self.item_model.repos, self.dialog
683)
684self.get_addon_score_worker.finished.connect(self.score_fetched_successfully)
685self.get_addon_score_worker.finished.connect(self.do_next_startup_phase)
686self.get_addon_score_worker.update_addon_score.connect(self.update_addon_score)
687self.get_addon_score_worker.start()
688else:
689self.composite_view.package_list.ui.view_bar.set_rankings_available(False)
690self.do_next_startup_phase()
691
692def update_addon_score(self, addon: Addon):
693self.item_model.reload_item(addon)
694
695def score_fetched_successfully(self):
696self.composite_view.package_list.ui.view_bar.set_rankings_available(True)
697
698def show_developer_tools(self) -> None:
699"""Display the developer tools dialog"""
700if not self.developer_mode:
701self.developer_mode = DeveloperMode()
702self.developer_mode.show()
703
704checker = MetadataValidators()
705checker.validate_all(self.item_model.repos)
706
707def add_addon_repo(self, addon_repo: Addon) -> None:
708"""adds a workbench to the list"""
709
710if addon_repo.icon is None or addon_repo.icon.isNull():
711addon_repo.icon = self.get_icon(addon_repo)
712for repo in self.item_model.repos:
713if repo.name == addon_repo.name:
714# self.item_model.reload_item(repo) # If we want to have later additions superseded
715# earlier
716return
717self.item_model.append_item(addon_repo)
718
719def 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,
721in which case the icon is regenerated."""
722
723if not update and repo.icon and not repo.icon.isNull() and repo.icon.isValid():
724return repo.icon
725
726path = ":/icons/" + repo.name.replace(" ", "_")
727if repo.repo_type == Addon.Kind.WORKBENCH:
728path += "_workbench_icon.svg"
729default_icon = QtGui.QIcon(":/icons/document-package.svg")
730elif repo.repo_type == Addon.Kind.MACRO:
731if repo.macro and repo.macro.icon:
732if os.path.isabs(repo.macro.icon):
733path = repo.macro.icon
734default_icon = QtGui.QIcon(":/icons/document-python.svg")
735else:
736path = os.path.join(os.path.dirname(repo.macro.src_filename), repo.macro.icon)
737default_icon = QtGui.QIcon(":/icons/document-python.svg")
738elif repo.macro and repo.macro.xpm:
739cache_path = FreeCAD.getUserCachePath()
740am_path = os.path.join(cache_path, "AddonManager", "MacroIcons")
741os.makedirs(am_path, exist_ok=True)
742path = os.path.join(am_path, repo.name + "_icon.xpm")
743if not os.path.exists(path):
744with open(path, "w") as f:
745f.write(repo.macro.xpm)
746default_icon = QtGui.QIcon(repo.macro.xpm)
747else:
748path += "_macro_icon.svg"
749default_icon = QtGui.QIcon(":/icons/document-python.svg")
750elif repo.repo_type == Addon.Kind.PACKAGE:
751# The cache might not have been downloaded yet, check to see if it's there...
752if os.path.isfile(repo.get_cached_icon_filename()):
753path = repo.get_cached_icon_filename()
754elif repo.contains_workbench():
755path += "_workbench_icon.svg"
756default_icon = QtGui.QIcon(":/icons/document-package.svg")
757elif repo.contains_macro():
758path += "_macro_icon.svg"
759default_icon = QtGui.QIcon(":/icons/document-python.svg")
760else:
761default_icon = QtGui.QIcon(":/icons/document-package.svg")
762
763if QtCore.QFile.exists(path):
764addonicon = QtGui.QIcon(path)
765else:
766addonicon = default_icon
767repo.icon = addonicon
768
769return addonicon
770
771def show_information(self, message: str) -> None:
772"""shows generic text in the information pane"""
773
774self.composite_view.package_list.ui.progressBar.set_status(message)
775self.composite_view.package_list.ui.progressBar.repaint()
776
777def append_to_repos_list(self, repo: Addon) -> None:
778"""this function allows threads to update the main list of workbenches"""
779self.item_model.append_item(repo)
780
781def update(self, repo: Addon) -> None:
782self.launch_installer_gui(repo)
783
784def mark_repo_update_available(self, repo: Addon, available: bool) -> None:
785if available:
786repo.set_status(Addon.Status.UPDATE_AVAILABLE)
787else:
788repo.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
789self.item_model.reload_item(repo)
790self.composite_view.package_details_controller.show_repo(repo)
791
792def launch_installer_gui(self, addon: Addon) -> None:
793if self.installer_gui is not None:
794FreeCAD.Console.PrintError(
795translate(
796"AddonsInstaller",
797"Cannot launch a new installer until the previous one has finished.",
798)
799)
800return
801if addon.macro is not None:
802self.installer_gui = MacroInstallerGUI(addon)
803else:
804self.installer_gui = AddonInstallerGUI(addon, self.item_model.repos)
805self.installer_gui.success.connect(self.on_package_status_changed)
806self.installer_gui.finished.connect(self.cleanup_installer)
807self.installer_gui.run() # Does not block
808
809def cleanup_installer(self) -> None:
810QtCore.QTimer.singleShot(500, self.no_really_clean_up_the_installer)
811
812def no_really_clean_up_the_installer(self) -> None:
813self.installer_gui = None
814
815def update_all(self) -> None:
816"""Asynchronously apply all available updates: individual failures are noted, but do not
817stop other updates"""
818
819if self.installer_gui is not None:
820FreeCAD.Console.PrintError(
821translate(
822"AddonsInstaller",
823"Cannot launch a new installer until the previous one has finished.",
824)
825)
826return
827
828self.installer_gui = UpdateAllGUI(self.item_model.repos)
829self.installer_gui.addon_updated.connect(self.on_package_status_changed)
830self.installer_gui.finished.connect(self.cleanup_installer)
831self.installer_gui.run() # Does not block
832
833def hide_progress_widgets(self) -> None:
834"""hides the progress bar and related widgets"""
835
836self.composite_view.package_list.ui.progressBar.hide()
837self.composite_view.package_list.ui.view_bar.search.setFocus()
838
839def show_progress_widgets(self) -> None:
840if self.composite_view.package_list.ui.progressBar.isHidden():
841self.composite_view.package_list.ui.progressBar.show()
842
843def update_progress_bar(self, current_value: int, max_value: int) -> None:
844"""Update the progress bar, showing it if it's hidden"""
845
846max_value = max_value if max_value > 0 else 1
847
848if current_value < 0:
849current_value = 0
850elif current_value > max_value:
851current_value = max_value
852
853self.show_progress_widgets()
854region_size = 100.0 / self.number_of_progress_regions
855completed_region_portion = (self.current_progress_region - 1) * region_size
856current_region_portion = (float(current_value) / float(max_value)) * region_size
857value = completed_region_portion + current_region_portion
858self.composite_view.package_list.ui.progressBar.set_value(
859value * 10
860) # Out of 1000 segments, so it moves sort of smoothly
861self.composite_view.package_list.ui.progressBar.repaint()
862
863def stop_update(self) -> None:
864self.cleanup_workers()
865self.hide_progress_widgets()
866self.write_cache_stopfile()
867self.button_bar.refresh_local_cache.setEnabled(True)
868self.button_bar.refresh_local_cache.setText(
869translate("AddonsInstaller", "Refresh local cache")
870)
871
872def write_cache_stopfile(self) -> None:
873stopfile = utils.get_cache_file_name("CACHE_UPDATE_INTERRUPTED")
874with open(stopfile, "w", encoding="utf8") as f:
875f.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
881def on_package_status_changed(self, repo: Addon) -> None:
882if repo.status() == Addon.Status.PENDING_RESTART:
883self.restart_required = True
884self.item_model.reload_item(repo)
885self.composite_view.package_details_controller.show_repo(repo)
886if repo in self.packages_with_updates:
887self.packages_with_updates.remove(repo)
888self.enable_updates(len(self.packages_with_updates))
889
890def executemacro(self, repo: Addon) -> None:
891"""executes a selected macro"""
892
893macro = repo.macro
894if not macro or not macro.code:
895return
896
897if macro.is_installed():
898macro_path = os.path.join(self.macro_repo_dir, macro.filename)
899FreeCADGui.open(str(macro_path))
900self.dialog.hide()
901FreeCADGui.SendMsgToActiveView("Run")
902else:
903with tempfile.TemporaryDirectory() as dir:
904temp_install_succeeded = macro.install(dir)
905if not temp_install_succeeded:
906message = translate(
907"AddonsInstaller",
908"Execution of macro failed. See console for failure details.",
909)
910return
911macro_path = os.path.join(dir, macro.filename)
912FreeCADGui.open(str(macro_path))
913self.dialog.hide()
914FreeCADGui.SendMsgToActiveView("Run")
915
916def remove(self, addon: Addon) -> None:
917"""Remove this addon."""
918if self.installer_gui is not None:
919FreeCAD.Console.PrintError(
920translate(
921"AddonsInstaller",
922"Cannot launch a new installer until the previous one has finished.",
923)
924)
925return
926self.installer_gui = AddonUninstallerGUI(addon)
927self.installer_gui.finished.connect(self.cleanup_installer)
928self.installer_gui.finished.connect(
929functools.partial(self.on_package_status_changed, addon)
930)
931self.installer_gui.run() # Does not block
932
933
934# @}
935