mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-07 15:07:28 -06:00
Fix downloadPresenter and initial LicensePresenter.py code
CURA-6983
This commit is contained in:
parent
028aece644
commit
dda3d0b4eb
5 changed files with 196 additions and 43 deletions
|
@ -13,6 +13,7 @@ import UM 1.1 as UM
|
|||
|
||||
UM.Dialog
|
||||
{
|
||||
id: licenseDialog
|
||||
title: catalog.i18nc("@title:window", "Plugin License Agreement")
|
||||
minimumWidth: UM.Theme.getSize("license_window_minimum").width
|
||||
minimumHeight: UM.Theme.getSize("license_window_minimum").height
|
||||
|
@ -21,16 +22,21 @@ UM.Dialog
|
|||
property var pluginName;
|
||||
property var licenseContent;
|
||||
property var pluginFileLocation;
|
||||
|
||||
Item
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
UM.I18nCatalog{id: catalog; name: "cura"}
|
||||
|
||||
|
||||
Label
|
||||
{
|
||||
id: licenseTitle
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
text: licenseDialog.pluginName + ": " + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?")
|
||||
text: licenseModel.title
|
||||
wrapMode: Text.Wrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
@ -43,7 +49,7 @@ UM.Dialog
|
|||
anchors.right: parent.right
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
readOnly: true
|
||||
text: licenseDialog.licenseContent || ""
|
||||
text: licenseModel.licenseText
|
||||
}
|
||||
}
|
||||
rightButtons:
|
||||
|
@ -53,22 +59,14 @@ UM.Dialog
|
|||
id: acceptButton
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
text: catalog.i18nc("@action:button", "Accept")
|
||||
onClicked:
|
||||
{
|
||||
licenseDialog.close();
|
||||
toolbox.install(licenseDialog.pluginFileLocation);
|
||||
toolbox.subscribe(licenseDialog.pluginName);
|
||||
}
|
||||
onClicked: handler.onLicenseAccepted
|
||||
},
|
||||
Button
|
||||
{
|
||||
id: declineButton
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
text: catalog.i18nc("@action:button", "Decline")
|
||||
onClicked:
|
||||
{
|
||||
licenseDialog.close();
|
||||
}
|
||||
onClicked: handler.onLicenseDeclined
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import tempfile
|
||||
from functools import reduce
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
|
@ -30,7 +30,7 @@ class DownloadPresenter:
|
|||
|
||||
self._started = False
|
||||
self._progress_message = None # type: Optional[Message]
|
||||
self._progress = {} # type: Dict[str, Dict[str, int]] # package_id, Dict
|
||||
self._progress = {} # type: Dict[str, Dict[str, Any]] # package_id, Dict
|
||||
self._error = [] # type: List[str] # package_id
|
||||
|
||||
def download(self, model: SubscribedPackagesModel):
|
||||
|
@ -41,26 +41,41 @@ class DownloadPresenter:
|
|||
manager = HttpRequestManager.getInstance()
|
||||
for item in model.items:
|
||||
package_id = item["package_id"]
|
||||
|
||||
request_data = manager.get(
|
||||
item["download_url"],
|
||||
callback = lambda reply, pid = package_id: self._onFinished(pid, reply),
|
||||
download_progress_callback = lambda rx, rt, pid = package_id: self._onProgress(pid, rx, rt),
|
||||
error_callback = lambda rx, rt, pid = package_id: self._onProgress(pid, rx, rt),
|
||||
scope = self._scope)
|
||||
|
||||
self._progress[package_id] = {
|
||||
"received": 0,
|
||||
"total": 1 # make sure this is not considered done yet. Also divByZero-safe
|
||||
"total": 1, # make sure this is not considered done yet. Also divByZero-safe
|
||||
"file_written": None,
|
||||
"request_data": request_data
|
||||
}
|
||||
|
||||
manager.get(
|
||||
item["download_url"],
|
||||
callback = lambda reply: self._onFinished(package_id, reply),
|
||||
download_progress_callback = lambda rx, rt: self._onProgress(package_id, rx, rt),
|
||||
error_callback = lambda rx, rt: self._onProgress(package_id, rx, rt),
|
||||
scope = self._scope)
|
||||
|
||||
self._started = True
|
||||
self._showProgressMessage()
|
||||
|
||||
def abort(self):
|
||||
manager = HttpRequestManager.getInstance()
|
||||
for item in self._progress.values():
|
||||
manager.abortRequest(item["request_data"])
|
||||
|
||||
# Aborts all current operations and returns a copy with the same settings such as app and scope
|
||||
def resetCopy(self):
|
||||
self.abort()
|
||||
self.done.disconnectAll()
|
||||
return DownloadPresenter(self._app)
|
||||
|
||||
def _showProgressMessage(self):
|
||||
self._progress_message = Message(i18n_catalog.i18nc(
|
||||
"@info:generic",
|
||||
"\nSyncing..."),
|
||||
lifetime = 0,
|
||||
use_inactivity_timer=False,
|
||||
progress = 0.0,
|
||||
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
||||
self._progress_message.show()
|
||||
|
@ -68,13 +83,21 @@ class DownloadPresenter:
|
|||
def _onFinished(self, package_id: str, reply: QNetworkReply):
|
||||
self._progress[package_id]["received"] = self._progress[package_id]["total"]
|
||||
|
||||
file_path = self._getTempFile(package_id)
|
||||
file_fd, file_path = tempfile.mkstemp()
|
||||
os.close(file_fd) # close the file so we can open it from python
|
||||
|
||||
try:
|
||||
with open(file_path) as temp_file:
|
||||
# todo buffer this
|
||||
temp_file.write(reply.readAll())
|
||||
except IOError:
|
||||
with open(file_path, "wb+") as temp_file:
|
||||
bytes_read = reply.read(256 * 1024)
|
||||
while bytes_read:
|
||||
temp_file.write(bytes_read)
|
||||
bytes_read = reply.read(256 * 1024)
|
||||
self._app.processEvents()
|
||||
self._progress[package_id]["file_written"] = file_path
|
||||
except IOError as e:
|
||||
Logger.logException("e", "Failed to write downloaded package to temp file", e)
|
||||
self._onError(package_id)
|
||||
temp_file.close()
|
||||
|
||||
self._checkDone()
|
||||
|
||||
|
@ -90,8 +113,6 @@ class DownloadPresenter:
|
|||
|
||||
self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
|
||||
|
||||
self._checkDone()
|
||||
|
||||
def _onError(self, package_id: str):
|
||||
self._progress.pop(package_id)
|
||||
self._error.append(package_id)
|
||||
|
@ -99,16 +120,11 @@ class DownloadPresenter:
|
|||
|
||||
def _checkDone(self) -> bool:
|
||||
for item in self._progress.values():
|
||||
if item["received"] != item["total"] or item["total"] == -1:
|
||||
if not item["file_written"]:
|
||||
return False
|
||||
|
||||
success_items = {package_id : self._getTempFile(package_id) for package_id in self._progress.keys()}
|
||||
success_items = {package_id : value["file_written"] for package_id, value in self._progress.items()}
|
||||
error_items = [package_id for package_id in self._error]
|
||||
|
||||
self._progress_message.hide()
|
||||
self.done.emit(success_items, error_items)
|
||||
|
||||
def _getTempFile(self, package_id: str) -> str:
|
||||
temp_dir = tempfile.gettempdir()
|
||||
return os.path.join(temp_dir, package_id)
|
||||
|
||||
|
|
30
plugins/Toolbox/src/CloudSync/LicenseModel.py
Normal file
30
plugins/Toolbox/src/CloudSync/LicenseModel.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||
|
||||
|
||||
# Model for the ToolboxLicenseDialog
|
||||
class LicenseModel(QObject):
|
||||
titleChanged = pyqtSignal()
|
||||
licenseTextChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, title: str = "", license_text: str = ""):
|
||||
super().__init__()
|
||||
self._title = title
|
||||
self._license_text = license_text
|
||||
|
||||
@pyqtProperty(str, notify=titleChanged)
|
||||
def title(self) -> str:
|
||||
return self._title
|
||||
|
||||
def setTitle(self, title: str) -> None:
|
||||
if self._title != title:
|
||||
self._title = title
|
||||
self.titleChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=licenseTextChanged)
|
||||
def licenseText(self) -> str:
|
||||
return self._license_text
|
||||
|
||||
def setLicenseText(self, license_text: str) -> None:
|
||||
if self._license_text != license_text:
|
||||
self._license_text = license_text
|
||||
self.licenseTextChanged.emit()
|
89
plugins/Toolbox/src/CloudSync/LicensePresenter.py
Normal file
89
plugins/Toolbox/src/CloudSync/LicensePresenter.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import os
|
||||
from typing import Dict, Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.PackageManager import PackageManager
|
||||
from UM.Signal import Signal
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
|
||||
from plugins.Toolbox.src.CloudSync.LicenseModel import LicenseModel
|
||||
|
||||
|
||||
class LicensePresenter(QObject):
|
||||
|
||||
def __init__(self, app: CuraApplication):
|
||||
super().__init__()
|
||||
self._dialog = None #type: Optional[QObject]
|
||||
self._package_manager = app.getPackageManager() # type: PackageManager
|
||||
# Emits # todo
|
||||
self.license_answers = Signal()
|
||||
|
||||
self._current_package_idx = 0
|
||||
self._package_models = None # type: Optional[Dict]
|
||||
|
||||
self._app = app
|
||||
|
||||
self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml"
|
||||
|
||||
## Show a license dialog for multiple packages where users can read a license and accept or decline them
|
||||
# \param packages: Dict[package id, file path]
|
||||
def present(self, plugin_path: str, packages: Dict[str, str]):
|
||||
path = os.path.join(plugin_path, self._compatibility_dialog_path)
|
||||
|
||||
self._initState(packages)
|
||||
|
||||
if self._dialog is None:
|
||||
|
||||
context_properties = {
|
||||
"catalog": i18nCatalog("cura"),
|
||||
"licenseModel": LicenseModel("initial title", "initial text"),
|
||||
"handler": self
|
||||
}
|
||||
self._dialog = self._app.createQmlComponent(path, context_properties)
|
||||
|
||||
self._present_current_package()
|
||||
|
||||
@pyqtSlot()
|
||||
def onLicenseAccepted(self):
|
||||
self._package_models[self._current_package_idx]["accepted"] = True
|
||||
self._check_next_page()
|
||||
|
||||
@pyqtSlot()
|
||||
def onLicenseDeclined(self):
|
||||
self._package_models[self._current_package_idx]["accepted"] = False
|
||||
self._check_next_page()
|
||||
|
||||
def _initState(self, packages: Dict[str, str]):
|
||||
self._package_models = [
|
||||
{
|
||||
"package_id" : package_id,
|
||||
"package_path" : package_path,
|
||||
"accepted" : None #: None: no answer yet
|
||||
}
|
||||
for package_id, package_path in packages.items()
|
||||
]
|
||||
|
||||
def _present_current_package(self):
|
||||
package_model = self._package_models[self._current_package_idx]
|
||||
license_content = self._package_manager.getPackageLicense(package_model["package_path"])
|
||||
if license_content is None:
|
||||
# implicitly accept when there is no license
|
||||
self.onLicenseAccepted()
|
||||
return
|
||||
|
||||
self._dialog.setProperty("licenseModel", LicenseModel("testTitle", "hoi"))
|
||||
self._dialog.open() # does nothing if already open
|
||||
|
||||
def _check_next_page(self):
|
||||
if self._current_package_idx + 1 < len(self._package_models):
|
||||
self._current_package_idx += 1
|
||||
self._present_current_package()
|
||||
else:
|
||||
self._dialog.close()
|
||||
self.license_answers.emit(self._package_models)
|
||||
|
||||
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
from typing import List, Dict
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from plugins.Toolbox import CloudPackageChecker
|
||||
from plugins.Toolbox.src.CloudSync.DiscrepanciesPresenter import DiscrepanciesPresenter
|
||||
from plugins.Toolbox.src.CloudSync.DownloadPresenter import DownloadPresenter
|
||||
from plugins.Toolbox.src.CloudSync.LicensePresenter import LicensePresenter
|
||||
from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
|
||||
|
@ -15,8 +18,8 @@ from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPack
|
|||
# the user selected to be performed
|
||||
# - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
|
||||
# - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads
|
||||
# - The LicencePresenter extracts licences from the downloaded packages and presents a licence for each package to
|
||||
# - be installed. It emits the `licenceAnswers` {'packageId' : bool} for accept or declines
|
||||
# - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
|
||||
# - be installed. It emits the `licenseAnswers` {'packageId' : bool} for accept or declines
|
||||
# - The CloudPackageManager removes the declined packages from the account
|
||||
# - The SyncOrchestrator uses PackageManager to install the downloaded packages.
|
||||
# - Bliss / profit / done
|
||||
|
@ -24,18 +27,35 @@ class SyncOrchestrator(Extension):
|
|||
|
||||
def __init__(self, app: CuraApplication):
|
||||
super().__init__()
|
||||
self._name = "SyncOrchestrator" # Critical to differentiate This PluginObject from the Toolbox
|
||||
|
||||
self._checker = CloudPackageChecker(app)
|
||||
self._checker = CloudPackageChecker(app) # type: CloudPackageChecker
|
||||
self._checker.discrepancies.connect(self._onDiscrepancies)
|
||||
|
||||
self._discrepanciesPresenter = DiscrepanciesPresenter(app)
|
||||
self._discrepanciesPresenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter
|
||||
self._discrepanciesPresenter.packageMutations.connect(self._onPackageMutations)
|
||||
|
||||
self._downloadPresenter = DownloadPresenter(app)
|
||||
self._downloadPresenter = DownloadPresenter(app) # type: DownloadPresenter
|
||||
|
||||
self._licensePresenter = LicensePresenter(app) # type: LicensePresenter
|
||||
|
||||
def _onDiscrepancies(self, model: SubscribedPackagesModel):
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
self._discrepanciesPresenter.present(plugin_path, model)
|
||||
# todo revert
|
||||
self._onDownloadFinished({"SupportEraser" : "/home/nvanhooff/Downloads/ThingiBrowser-v7.0.0-2019-12-12T18_24_40Z.curapackage"}, [])
|
||||
# plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
# self._discrepanciesPresenter.present(plugin_path, model)
|
||||
|
||||
def _onPackageMutations(self, mutations: SubscribedPackagesModel):
|
||||
self._downloadPresenter = self._downloadPresenter.resetCopy()
|
||||
self._downloadPresenter.done.connect(self._onDownloadFinished)
|
||||
self._downloadPresenter.download(mutations)
|
||||
|
||||
## When a set of packages have finished downloading
|
||||
# \param success_items: Dict[package_id, file_path]
|
||||
# \param error_items: List[package_id]
|
||||
def _onDownloadFinished(self, success_items: Dict[str, str], error_items: List[str]):
|
||||
# todo handle error items
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
self._licensePresenter.present(plugin_path, success_items)
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue