RemovableDriveOutputDevice.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. # Copyright (c) 2016 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. import os.path
  4. from UM.Application import Application
  5. from UM.Logger import Logger
  6. from UM.Message import Message
  7. from UM.FileHandler.WriteFileJob import WriteFileJob
  8. from UM.Mesh.MeshWriter import MeshWriter
  9. from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
  10. from UM.OutputDevice.OutputDevice import OutputDevice
  11. from UM.OutputDevice import OutputDeviceError
  12. from UM.i18n import i18nCatalog
  13. catalog = i18nCatalog("cura")
  14. class RemovableDriveOutputDevice(OutputDevice):
  15. def __init__(self, device_id, device_name):
  16. super().__init__(device_id)
  17. self.setName(device_name)
  18. self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "Save to Removable Drive"))
  19. self.setDescription(catalog.i18nc("@item:inlistbox", "Save to Removable Drive {0}").format(device_name))
  20. self.setIconName("save_sd")
  21. self.setPriority(1)
  22. self._writing = False
  23. self._stream = None
  24. ## Request the specified nodes to be written to the removable drive.
  25. #
  26. # \param nodes A collection of scene nodes that should be written to the
  27. # removable drive.
  28. # \param file_name \type{string} A suggestion for the file name to write
  29. # to. If none is provided, a file name will be made from the names of the
  30. # meshes.
  31. # \param limit_mimetypes Should we limit the available MIME types to the
  32. # MIME types available to the currently active machine?
  33. #
  34. def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
  35. filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
  36. if self._writing:
  37. raise OutputDeviceError.DeviceBusyError()
  38. # Formats supported by this application (File types that we can actually write)
  39. if file_handler:
  40. file_formats = file_handler.getSupportedFileTypesWrite()
  41. else:
  42. file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
  43. if filter_by_machine:
  44. container = Application.getInstance().getGlobalContainerStack().findContainer({"file_formats": "*"})
  45. # Create a list from supported file formats string
  46. machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")]
  47. # Take the intersection between file_formats and machine_file_formats.
  48. file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats))
  49. if len(file_formats) == 0:
  50. Logger.log("e", "There are no file formats available to write with!")
  51. raise OutputDeviceError.WriteRequestFailedError()
  52. # Just take the first file format available.
  53. if file_handler is not None:
  54. writer = file_handler.getWriterByMimeType(file_formats[0]["mime_type"])
  55. else:
  56. writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
  57. extension = file_formats[0]["extension"]
  58. if file_name is None:
  59. file_name = self._automaticFileName(nodes)
  60. if extension: # Not empty string.
  61. extension = "." + extension
  62. file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + extension)
  63. try:
  64. Logger.log("d", "Writing to %s", file_name)
  65. # Using buffering greatly reduces the write time for many lines of gcode
  66. self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
  67. job = WriteFileJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode)
  68. job.setFileName(file_name)
  69. job.progress.connect(self._onProgress)
  70. job.finished.connect(self._onFinished)
  71. message = Message(catalog.i18nc("@info:progress", "Saving to Removable Drive <filename>{0}</filename>").format(self.getName()), 0, False, -1)
  72. message.show()
  73. self.writeStarted.emit(self)
  74. job.setMessage(message)
  75. self._writing = True
  76. job.start()
  77. except PermissionError as e:
  78. Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e))
  79. raise OutputDeviceError.PermissionDeniedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
  80. except OSError as e:
  81. Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
  82. raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
  83. ## Generate a file name automatically for the specified nodes to be saved
  84. # in.
  85. #
  86. # The name generated will be the name of one of the nodes. Which node that
  87. # is can not be guaranteed.
  88. #
  89. # \param nodes A collection of nodes for which to generate a file name.
  90. def _automaticFileName(self, nodes):
  91. for root in nodes:
  92. for child in BreadthFirstIterator(root):
  93. if child.getMeshData():
  94. name = child.getName()
  95. if name:
  96. return name
  97. raise OutputDeviceError.WriteRequestFailedError("Could not find a file name when trying to write to {device}.".format(device = self.getName()))
  98. def _onProgress(self, job, progress):
  99. self.writeProgress.emit(self, progress)
  100. def _onFinished(self, job):
  101. if self._stream:
  102. # Explicitly closing the stream flushes the write-buffer
  103. self._stream.close()
  104. self._stream = None
  105. self._writing = False
  106. self.writeFinished.emit(self)
  107. if job.getResult():
  108. message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())))
  109. message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
  110. message.actionTriggered.connect(self._onActionTriggered)
  111. message.show()
  112. self.writeSuccess.emit(self)
  113. else:
  114. message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(), str(job.getError())))
  115. message.show()
  116. self.writeError.emit(self)
  117. job.getStream().close()
  118. def _onActionTriggered(self, message, action):
  119. if action == "eject":
  120. if Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self):
  121. message.hide()
  122. eject_message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(self.getName()))
  123. else:
  124. eject_message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Another program may be using the drive.").format(self.getName()))
  125. eject_message.show()