CloudMaterialSync.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. # Copyright (c) 2021 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
  4. from PyQt5.QtGui import QDesktopServices
  5. from typing import Optional, TYPE_CHECKING
  6. import zipfile # To export all materials in a .zip archive.
  7. import cura.CuraApplication # Imported like this to prevent circular imports.
  8. from cura.PrinterOutput.UploadMaterialsJob import UploadMaterialsJob # To export materials to the output printer.
  9. from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
  10. from UM.i18n import i18nCatalog
  11. from UM.Logger import Logger
  12. from UM.Message import Message
  13. if TYPE_CHECKING:
  14. from UM.Signal import Signal
  15. catalog = i18nCatalog("cura")
  16. class CloudMaterialSync(QObject):
  17. """
  18. Handles the synchronisation of material profiles with cloud accounts.
  19. """
  20. def __init__(self, parent: QObject = None):
  21. super().__init__(parent)
  22. self.sync_all_dialog = None # type: Optional[QObject]
  23. self._export_upload_status = "idle"
  24. self._checkIfNewMaterialsWereInstalled()
  25. self._export_progress = 0
  26. def _checkIfNewMaterialsWereInstalled(self) -> None:
  27. """
  28. Checks whether new material packages were installed in the latest startup. If there were, then it shows
  29. a message prompting the user to sync the materials with their printers.
  30. """
  31. application = cura.CuraApplication.CuraApplication.getInstance()
  32. for package_id, package_data in application.getPackageManager().getPackagesInstalledOnStartup().items():
  33. if package_data["package_info"]["package_type"] == "material":
  34. # At least one new material was installed
  35. self._showSyncNewMaterialsMessage()
  36. break
  37. def _showSyncNewMaterialsMessage(self) -> None:
  38. sync_materials_message = Message(
  39. text = catalog.i18nc("@action:button",
  40. "Please sync the material profiles with your printers before starting to print."),
  41. title = catalog.i18nc("@action:button", "New materials installed"),
  42. message_type = Message.MessageType.WARNING,
  43. lifetime = 0
  44. )
  45. sync_materials_message.addAction(
  46. "sync",
  47. name = catalog.i18nc("@action:button", "Sync materials with printers"),
  48. icon = "",
  49. description = "Sync your newly installed materials with your printers.",
  50. button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
  51. )
  52. sync_materials_message.addAction(
  53. "learn_more",
  54. name = catalog.i18nc("@action:button", "Learn more"),
  55. icon = "",
  56. description = "Learn more about syncing your newly installed materials with your printers.",
  57. button_align = Message.ActionButtonAlignment.ALIGN_LEFT,
  58. button_style = Message.ActionButtonStyle.LINK
  59. )
  60. sync_materials_message.actionTriggered.connect(self._onSyncMaterialsMessageActionTriggered)
  61. # Show the message only if there are printers that support material export
  62. container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
  63. global_stacks = container_registry.findContainerStacks(type = "machine")
  64. if any([stack.supportsMaterialExport for stack in global_stacks]):
  65. sync_materials_message.show()
  66. def _onSyncMaterialsMessageActionTriggered(self, sync_message: Message, sync_message_action: str):
  67. if sync_message_action == "sync":
  68. self.openSyncAllWindow()
  69. sync_message.hide()
  70. elif sync_message_action == "learn_more":
  71. QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
  72. @pyqtSlot(result = QUrl)
  73. def getPreferredExportAllPath(self) -> QUrl:
  74. """
  75. Get the preferred path to export materials to.
  76. If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
  77. file path.
  78. :return: The preferred path to export all materials to.
  79. """
  80. cura_application = cura.CuraApplication.CuraApplication.getInstance()
  81. device_manager = cura_application.getOutputDeviceManager()
  82. devices = device_manager.getOutputDevices()
  83. for device in devices:
  84. if device.__class__.__name__ == "RemovableDriveOutputDevice":
  85. return QUrl.fromLocalFile(device.getId())
  86. else: # No removable drives? Use local path.
  87. return cura_application.getDefaultPath("dialog_material_path")
  88. @pyqtSlot(QUrl)
  89. def exportAll(self, file_path: QUrl, notify_progress: Optional["Signal"] = None) -> None:
  90. """
  91. Export all materials to a certain file path.
  92. :param file_path: The path to export the materials to.
  93. """
  94. registry = CuraContainerRegistry.getInstance()
  95. # Create empty archive.
  96. try:
  97. archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
  98. except OSError as e:
  99. Logger.log("e", f"Can't write to destination {file_path.toLocalFile()}: {type(e)} - {str(e)}")
  100. error_message = Message(
  101. text = catalog.i18nc("@message:text", "Could not save material archive to {}:").format(file_path.toLocalFile()) + " " + str(e),
  102. title = catalog.i18nc("@message:title", "Failed to save material archive"),
  103. message_type = Message.MessageType.ERROR
  104. )
  105. error_message.show()
  106. return
  107. materials_metadata = registry.findInstanceContainersMetadata(type = "material")
  108. for index, metadata in enumerate(materials_metadata):
  109. if notify_progress is not None:
  110. progress = index / len(materials_metadata)
  111. notify_progress.emit(progress)
  112. if metadata["base_file"] != metadata["id"]: # Only process base files.
  113. continue
  114. if metadata["id"] == "empty_material": # Don't export the empty material.
  115. continue
  116. material = registry.findContainers(id = metadata["id"])[0]
  117. suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
  118. filename = metadata["id"] + "." + suffix
  119. try:
  120. archive.writestr(filename, material.serialize())
  121. except OSError as e:
  122. Logger.log("e", f"An error has occurred while writing the material \'{metadata['id']}\' in the file \'{filename}\': {e}.")
  123. exportUploadStatusChanged = pyqtSignal()
  124. @pyqtProperty(str, notify = exportUploadStatusChanged)
  125. def exportUploadStatus(self) -> str:
  126. return self._export_upload_status
  127. @pyqtSlot()
  128. def exportUpload(self) -> None:
  129. """
  130. Export all materials and upload them to the user's account.
  131. """
  132. self._export_upload_status = "uploading"
  133. self.exportUploadStatusChanged.emit()
  134. job = UploadMaterialsJob(self)
  135. job.uploadProgressChanged.connect(self.setExportProgress)
  136. job.uploadCompleted.connect(self.exportUploadCompleted)
  137. job.start()
  138. def exportUploadCompleted(self, job_result: UploadMaterialsJob.Result):
  139. if job_result == UploadMaterialsJob.Result.FAILED:
  140. self.sync_all_dialog.setProperty("syncStatusText", catalog.i18nc("@text", "Something went wrong when sending the materials to the printers."))
  141. self._export_upload_status = "error"
  142. else:
  143. self._export_upload_status = "success"
  144. self.exportUploadStatusChanged.emit()
  145. exportProgressChanged = pyqtSignal(float)
  146. def setExportProgress(self, progress: float) -> None:
  147. self._export_progress = progress
  148. self.exportProgressChanged.emit(self._export_progress)
  149. @pyqtProperty(float, fset = setExportProgress, notify = exportProgressChanged)
  150. def exportProgress(self) -> float:
  151. return self._export_progress