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
|
UM.Dialog
|
||||||
{
|
{
|
||||||
|
id: licenseDialog
|
||||||
title: catalog.i18nc("@title:window", "Plugin License Agreement")
|
title: catalog.i18nc("@title:window", "Plugin License Agreement")
|
||||||
minimumWidth: UM.Theme.getSize("license_window_minimum").width
|
minimumWidth: UM.Theme.getSize("license_window_minimum").width
|
||||||
minimumHeight: UM.Theme.getSize("license_window_minimum").height
|
minimumHeight: UM.Theme.getSize("license_window_minimum").height
|
||||||
|
@ -21,16 +22,21 @@ UM.Dialog
|
||||||
property var pluginName;
|
property var pluginName;
|
||||||
property var licenseContent;
|
property var licenseContent;
|
||||||
property var pluginFileLocation;
|
property var pluginFileLocation;
|
||||||
|
|
||||||
Item
|
Item
|
||||||
{
|
{
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
UM.I18nCatalog{id: catalog; name: "cura"}
|
||||||
|
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
id: licenseTitle
|
id: licenseTitle
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
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
|
wrapMode: Text.Wrap
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
}
|
}
|
||||||
|
@ -43,7 +49,7 @@ UM.Dialog
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||||
readOnly: true
|
readOnly: true
|
||||||
text: licenseDialog.licenseContent || ""
|
text: licenseModel.licenseText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rightButtons:
|
rightButtons:
|
||||||
|
@ -53,22 +59,14 @@ UM.Dialog
|
||||||
id: acceptButton
|
id: acceptButton
|
||||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||||
text: catalog.i18nc("@action:button", "Accept")
|
text: catalog.i18nc("@action:button", "Accept")
|
||||||
onClicked:
|
onClicked: handler.onLicenseAccepted
|
||||||
{
|
|
||||||
licenseDialog.close();
|
|
||||||
toolbox.install(licenseDialog.pluginFileLocation);
|
|
||||||
toolbox.subscribe(licenseDialog.pluginName);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Button
|
Button
|
||||||
{
|
{
|
||||||
id: declineButton
|
id: declineButton
|
||||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||||
text: catalog.i18nc("@action:button", "Decline")
|
text: catalog.i18nc("@action:button", "Decline")
|
||||||
onClicked:
|
onClicked: handler.onLicenseDeclined
|
||||||
{
|
|
||||||
licenseDialog.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Any
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QNetworkReply
|
from PyQt5.QtNetwork import QNetworkReply
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class DownloadPresenter:
|
||||||
|
|
||||||
self._started = False
|
self._started = False
|
||||||
self._progress_message = None # type: Optional[Message]
|
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
|
self._error = [] # type: List[str] # package_id
|
||||||
|
|
||||||
def download(self, model: SubscribedPackagesModel):
|
def download(self, model: SubscribedPackagesModel):
|
||||||
|
@ -41,26 +41,41 @@ class DownloadPresenter:
|
||||||
manager = HttpRequestManager.getInstance()
|
manager = HttpRequestManager.getInstance()
|
||||||
for item in model.items:
|
for item in model.items:
|
||||||
package_id = item["package_id"]
|
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] = {
|
self._progress[package_id] = {
|
||||||
"received": 0,
|
"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._started = True
|
||||||
self._showProgressMessage()
|
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):
|
def _showProgressMessage(self):
|
||||||
self._progress_message = Message(i18n_catalog.i18nc(
|
self._progress_message = Message(i18n_catalog.i18nc(
|
||||||
"@info:generic",
|
"@info:generic",
|
||||||
"\nSyncing..."),
|
"\nSyncing..."),
|
||||||
lifetime = 0,
|
lifetime = 0,
|
||||||
|
use_inactivity_timer=False,
|
||||||
progress = 0.0,
|
progress = 0.0,
|
||||||
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
||||||
self._progress_message.show()
|
self._progress_message.show()
|
||||||
|
@ -68,13 +83,21 @@ class DownloadPresenter:
|
||||||
def _onFinished(self, package_id: str, reply: QNetworkReply):
|
def _onFinished(self, package_id: str, reply: QNetworkReply):
|
||||||
self._progress[package_id]["received"] = self._progress[package_id]["total"]
|
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:
|
try:
|
||||||
with open(file_path) as temp_file:
|
with open(file_path, "wb+") as temp_file:
|
||||||
# todo buffer this
|
bytes_read = reply.read(256 * 1024)
|
||||||
temp_file.write(reply.readAll())
|
while bytes_read:
|
||||||
except IOError:
|
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)
|
self._onError(package_id)
|
||||||
|
temp_file.close()
|
||||||
|
|
||||||
self._checkDone()
|
self._checkDone()
|
||||||
|
|
||||||
|
@ -90,8 +113,6 @@ class DownloadPresenter:
|
||||||
|
|
||||||
self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
|
self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
|
||||||
|
|
||||||
self._checkDone()
|
|
||||||
|
|
||||||
def _onError(self, package_id: str):
|
def _onError(self, package_id: str):
|
||||||
self._progress.pop(package_id)
|
self._progress.pop(package_id)
|
||||||
self._error.append(package_id)
|
self._error.append(package_id)
|
||||||
|
@ -99,16 +120,11 @@ class DownloadPresenter:
|
||||||
|
|
||||||
def _checkDone(self) -> bool:
|
def _checkDone(self) -> bool:
|
||||||
for item in self._progress.values():
|
for item in self._progress.values():
|
||||||
if item["received"] != item["total"] or item["total"] == -1:
|
if not item["file_written"]:
|
||||||
return False
|
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]
|
error_items = [package_id for package_id in self._error]
|
||||||
|
|
||||||
self._progress_message.hide()
|
self._progress_message.hide()
|
||||||
self.done.emit(success_items, error_items)
|
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.Extension import Extension
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from plugins.Toolbox import CloudPackageChecker
|
from plugins.Toolbox import CloudPackageChecker
|
||||||
from plugins.Toolbox.src.CloudSync.DiscrepanciesPresenter import DiscrepanciesPresenter
|
from plugins.Toolbox.src.CloudSync.DiscrepanciesPresenter import DiscrepanciesPresenter
|
||||||
from plugins.Toolbox.src.CloudSync.DownloadPresenter import DownloadPresenter
|
from plugins.Toolbox.src.CloudSync.DownloadPresenter import DownloadPresenter
|
||||||
|
from plugins.Toolbox.src.CloudSync.LicensePresenter import LicensePresenter
|
||||||
from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel
|
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 user selected to be performed
|
||||||
# - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
|
# - 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 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
|
# - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
|
||||||
# - be installed. It emits the `licenceAnswers` {'packageId' : bool} for accept or declines
|
# - be installed. It emits the `licenseAnswers` {'packageId' : bool} for accept or declines
|
||||||
# - The CloudPackageManager removes the declined packages from the account
|
# - The CloudPackageManager removes the declined packages from the account
|
||||||
# - The SyncOrchestrator uses PackageManager to install the downloaded packages.
|
# - The SyncOrchestrator uses PackageManager to install the downloaded packages.
|
||||||
# - Bliss / profit / done
|
# - Bliss / profit / done
|
||||||
|
@ -24,18 +27,35 @@ class SyncOrchestrator(Extension):
|
||||||
|
|
||||||
def __init__(self, app: CuraApplication):
|
def __init__(self, app: CuraApplication):
|
||||||
super().__init__()
|
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._checker.discrepancies.connect(self._onDiscrepancies)
|
||||||
|
|
||||||
self._discrepanciesPresenter = DiscrepanciesPresenter(app)
|
self._discrepanciesPresenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter
|
||||||
self._discrepanciesPresenter.packageMutations.connect(self._onPackageMutations)
|
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):
|
def _onDiscrepancies(self, model: SubscribedPackagesModel):
|
||||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
# todo revert
|
||||||
self._discrepanciesPresenter.present(plugin_path, model)
|
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):
|
def _onPackageMutations(self, mutations: SubscribedPackagesModel):
|
||||||
|
self._downloadPresenter = self._downloadPresenter.resetCopy()
|
||||||
|
self._downloadPresenter.done.connect(self._onDownloadFinished)
|
||||||
self._downloadPresenter.download(mutations)
|
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