FreeCAD
211 строк · 8.7 Кб
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"""Class to manage the display of an Update All dialog."""
25
26from enum import IntEnum, auto
27import os
28from typing import List
29
30import FreeCAD
31import FreeCADGui
32
33from PySide import QtCore, QtWidgets
34
35from Addon import Addon
36
37from addonmanager_installer import AddonInstaller, MacroInstaller
38
39translate = FreeCAD.Qt.translate
40
41# pylint: disable=too-few-public-methods,too-many-instance-attributes
42
43
44class UpdaterFactory:
45"""A factory class for generating updaters. Mainly exists to allow easily mocking
46those updaters during testing. A replacement class need only provide a
47"get_updater" function that returns mock updater objects. Those objects must be
48QObjects with a run() function and a finished signal."""
49
50def __init__(self, addons):
51self.addons = addons
52
53def get_updater(self, addon):
54"""Get an updater for this addon (either a MacroInstaller or an
55AddonInstaller)"""
56if addon.macro is not None:
57return MacroInstaller(addon)
58return AddonInstaller(addon, self.addons)
59
60
61class AddonStatus(IntEnum):
62"""The current status of the installation process for a given addon"""
63
64WAITING = auto()
65INSTALLING = auto()
66SUCCEEDED = auto()
67FAILED = auto()
68
69def ui_string(self):
70"""Get the string that the UI should show for this status"""
71if self.value == AddonStatus.WAITING:
72return ""
73if self.value == AddonStatus.INSTALLING:
74return translate("AddonsInstaller", "Installing") + "..."
75if self.value == AddonStatus.SUCCEEDED:
76return translate("AddonsInstaller", "Succeeded")
77if self.value == AddonStatus.FAILED:
78return translate("AddonsInstaller", "Failed")
79return "[INTERNAL ERROR]"
80
81
82class UpdateAllGUI(QtCore.QObject):
83"""A GUI to display and manage an "update all" process."""
84
85finished = QtCore.Signal()
86addon_updated = QtCore.Signal(object)
87
88def __init__(self, addons: List[Addon]):
89super().__init__()
90self.addons = addons
91self.dialog = FreeCADGui.PySideUic.loadUi(
92os.path.join(os.path.dirname(__file__), "update_all.ui")
93)
94self.dialog.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True)
95self.row_map = {}
96self.in_process_row = None
97self.active_installer = None
98self.addons_with_update: List[Addon] = []
99self.updater_factory = UpdaterFactory(addons)
100self.worker_thread = None
101self.running = False
102self.cancelled = False
103
104def run(self):
105"""Run the Update All process. Blocks until updates are complete or
106cancelled."""
107self.running = True
108self._setup_dialog()
109self.dialog.show()
110self._process_next_update()
111
112def _setup_dialog(self):
113"""Prepare the dialog for display"""
114self.dialog.rejected.connect(self._cancel_installation)
115self.dialog.tableWidget.clear()
116self.in_process_row = None
117self.row_map = {}
118for addon in self.addons:
119if addon.status() == Addon.Status.UPDATE_AVAILABLE:
120self._add_addon_to_table(addon)
121self.addons_with_update.append(addon)
122
123def _cancel_installation(self):
124self.cancelled = True
125if self.worker_thread and self.worker_thread.isRunning():
126self.worker_thread.requestInterruption()
127
128def _add_addon_to_table(self, addon: Addon):
129"""Add the given addon to the list, with no icon in the first column"""
130new_row = self.dialog.tableWidget.rowCount()
131self.dialog.tableWidget.setColumnCount(2)
132self.dialog.tableWidget.setRowCount(new_row + 1)
133self.dialog.tableWidget.setItem(new_row, 0, QtWidgets.QTableWidgetItem(addon.display_name))
134self.dialog.tableWidget.setItem(new_row, 1, QtWidgets.QTableWidgetItem(""))
135self.row_map[addon.name] = new_row
136
137def _update_addon_status(self, row: int, status: AddonStatus):
138"""Update the GUI to reflect this addon's new status."""
139self.dialog.tableWidget.item(row, 1).setText(status.ui_string())
140
141def _process_next_update(self):
142"""Grab the next addon in the list and start its updater."""
143if self.addons_with_update:
144addon = self.addons_with_update.pop(0)
145self.in_process_row = self.row_map[addon.name] if addon.name in self.row_map else None
146self._update_addon_status(self.in_process_row, AddonStatus.INSTALLING)
147self.dialog.tableWidget.scrollToItem(
148self.dialog.tableWidget.item(self.in_process_row, 0)
149)
150self.active_installer = self.updater_factory.get_updater(addon)
151self._launch_active_installer()
152else:
153self._finalize()
154
155def _launch_active_installer(self):
156"""Set up and run the active installer in a new thread."""
157
158self.active_installer.success.connect(self._update_succeeded)
159self.active_installer.failure.connect(self._update_failed)
160self.active_installer.finished.connect(self._update_finished)
161
162self.worker_thread = QtCore.QThread()
163self.active_installer.moveToThread(self.worker_thread)
164self.worker_thread.started.connect(self.active_installer.run)
165self.worker_thread.start()
166
167def _update_succeeded(self, addon):
168"""Callback for a successful update"""
169self._update_addon_status(self.row_map[addon.name], AddonStatus.SUCCEEDED)
170self.addon_updated.emit(addon)
171
172def _update_failed(self, addon):
173"""Callback for a failed update"""
174self._update_addon_status(self.row_map[addon.name], AddonStatus.FAILED)
175
176def _update_finished(self):
177"""Callback for updater that has finished all its work"""
178if self.worker_thread is not None and self.worker_thread.isRunning():
179self.worker_thread.quit()
180self.worker_thread.wait()
181self.addon_updated.emit(self.active_installer.addon_to_install)
182if not self.cancelled:
183self._process_next_update()
184else:
185self._setup_cancelled_state()
186
187def _finalize(self):
188"""No more updates, clean up and shut down"""
189if self.worker_thread is not None and self.worker_thread.isRunning():
190self.worker_thread.quit()
191self.worker_thread.wait()
192text = translate("Addons installer", "Finished updating the following addons")
193self._set_dialog_to_final_state(text)
194self.running = False
195self.finished.emit()
196
197def _setup_cancelled_state(self):
198text1 = translate("AddonsInstaller", "Update was cancelled")
199text2 = translate("AddonsInstaller", "some addons may have been updated")
200self._set_dialog_to_final_state(text1 + ": " + text2)
201self.running = False
202self.finished.emit()
203
204def _set_dialog_to_final_state(self, new_content):
205self.dialog.buttonBox.clear()
206self.dialog.buttonBox.addButton(QtWidgets.QDialogButtonBox.Close)
207self.dialog.label.setText(new_content)
208
209def is_running(self):
210"""True if the thread is running, and False if not"""
211return self.running
212