FreeCAD
210 строк · 8.6 Кб
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.row_map = {}
95self.in_process_row = None
96self.active_installer = None
97self.addons_with_update: List[Addon] = []
98self.updater_factory = UpdaterFactory(addons)
99self.worker_thread = None
100self.running = False
101self.cancelled = False
102
103def run(self):
104"""Run the Update All process. Blocks until updates are complete or
105cancelled."""
106self.running = True
107self._setup_dialog()
108self.dialog.show()
109self._process_next_update()
110
111def _setup_dialog(self):
112"""Prepare the dialog for display"""
113self.dialog.rejected.connect(self._cancel_installation)
114self.dialog.tableWidget.clear()
115self.in_process_row = None
116self.row_map = {}
117for addon in self.addons:
118if addon.status() == Addon.Status.UPDATE_AVAILABLE:
119self._add_addon_to_table(addon)
120self.addons_with_update.append(addon)
121
122def _cancel_installation(self):
123self.cancelled = True
124if self.worker_thread and self.worker_thread.isRunning():
125self.worker_thread.requestInterruption()
126
127def _add_addon_to_table(self, addon: Addon):
128"""Add the given addon to the list, with no icon in the first column"""
129new_row = self.dialog.tableWidget.rowCount()
130self.dialog.tableWidget.setColumnCount(2)
131self.dialog.tableWidget.setRowCount(new_row + 1)
132self.dialog.tableWidget.setItem(new_row, 0, QtWidgets.QTableWidgetItem(addon.display_name))
133self.dialog.tableWidget.setItem(new_row, 1, QtWidgets.QTableWidgetItem(""))
134self.row_map[addon.name] = new_row
135
136def _update_addon_status(self, row: int, status: AddonStatus):
137"""Update the GUI to reflect this addon's new status."""
138self.dialog.tableWidget.item(row, 1).setText(status.ui_string())
139
140def _process_next_update(self):
141"""Grab the next addon in the list and start its updater."""
142if self.addons_with_update:
143addon = self.addons_with_update.pop(0)
144self.in_process_row = self.row_map[addon.name] if addon.name in self.row_map else None
145self._update_addon_status(self.in_process_row, AddonStatus.INSTALLING)
146self.dialog.tableWidget.scrollToItem(
147self.dialog.tableWidget.item(self.in_process_row, 0)
148)
149self.active_installer = self.updater_factory.get_updater(addon)
150self._launch_active_installer()
151else:
152self._finalize()
153
154def _launch_active_installer(self):
155"""Set up and run the active installer in a new thread."""
156
157self.active_installer.success.connect(self._update_succeeded)
158self.active_installer.failure.connect(self._update_failed)
159self.active_installer.finished.connect(self._update_finished)
160
161self.worker_thread = QtCore.QThread()
162self.active_installer.moveToThread(self.worker_thread)
163self.worker_thread.started.connect(self.active_installer.run)
164self.worker_thread.start()
165
166def _update_succeeded(self, addon):
167"""Callback for a successful update"""
168self._update_addon_status(self.row_map[addon.name], AddonStatus.SUCCEEDED)
169self.addon_updated.emit(addon)
170
171def _update_failed(self, addon):
172"""Callback for a failed update"""
173self._update_addon_status(self.row_map[addon.name], AddonStatus.FAILED)
174
175def _update_finished(self):
176"""Callback for updater that has finished all its work"""
177if self.worker_thread is not None and self.worker_thread.isRunning():
178self.worker_thread.quit()
179self.worker_thread.wait()
180self.addon_updated.emit(self.active_installer.addon_to_install)
181if not self.cancelled:
182self._process_next_update()
183else:
184self._setup_cancelled_state()
185
186def _finalize(self):
187"""No more updates, clean up and shut down"""
188if self.worker_thread is not None and self.worker_thread.isRunning():
189self.worker_thread.quit()
190self.worker_thread.wait()
191text = translate("Addons installer", "Finished updating the following addons")
192self._set_dialog_to_final_state(text)
193self.running = False
194self.finished.emit()
195
196def _setup_cancelled_state(self):
197text1 = translate("AddonsInstaller", "Update was cancelled")
198text2 = translate("AddonsInstaller", "some addons may have been updated")
199self._set_dialog_to_final_state(text1 + ": " + text2)
200self.running = False
201self.finished.emit()
202
203def _set_dialog_to_final_state(self, new_content):
204self.dialog.buttonBox.clear()
205self.dialog.buttonBox.addButton(QtWidgets.QDialogButtonBox.Close)
206self.dialog.label.setText(new_content)
207
208def is_running(self):
209"""True if the thread is running, and False if not"""
210return self.running
211