diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 876bbeae3b..9da84163e6 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -67,7 +67,7 @@ class CuraApplication(QtApplication): "SelectionTool", "CameraTool", "GCodeWriter", - "LocalFileStorage" + "LocalFileOutputDevice" ]) self._physics = None self._volume = None @@ -101,16 +101,12 @@ class CuraApplication(QtApplication): self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib", "cura")) if not hasattr(sys, "frozen"): self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins")) + self._plugin_registry.loadPlugin("ConsoleLogger") - self._plugin_registry.loadPlugins({ "type": "logger"}) - self._plugin_registry.loadPlugins({ "type": "storage_device" }) - self._plugin_registry.loadPlugins({ "type": "view" }) - self._plugin_registry.loadPlugins({ "type": "mesh_reader" }) - self._plugin_registry.loadPlugins({ "type": "mesh_writer" }) - self._plugin_registry.loadPlugins({ "type": "tool" }) - self._plugin_registry.loadPlugins({ "type": "extension" }) + self._plugin_registry.loadPlugins() - self._plugin_registry.loadPlugin("CuraEngineBackend") + if self.getBackend() == None: + raise RuntimeError("Could not load the backend plugin!") def addCommandLineOptions(self, parser): super().addCommandLineOptions(parser) @@ -119,15 +115,6 @@ class CuraApplication(QtApplication): def run(self): self._i18n_catalog = i18nCatalog("cura"); - self.addOutputDevice("local_file", { - "id": "local_file", - "function": self._writeToLocalFile, - "description": self._i18n_catalog.i18nc("Save button tooltip", "Save to Disk"), - "shortDescription": self._i18n_catalog.i18nc("Save button tooltip", "Save to Disk"), - "icon": "save", - "priority": 0 - }) - self.showSplashMessage(self._i18n_catalog.i18nc("Splash screen message", "Setting up scene...")) controller = self.getController() @@ -167,8 +154,6 @@ class CuraApplication(QtApplication): self.setMainQml(Resources.getPath(Resources.QmlFilesLocation, "Cura.qml")) self.initializeEngine() - self.getStorageDevice("LocalFileStorage").removableDrivesChanged.connect(self._removableDrivesChanged) - if self.getMachines(): active_machine_pref = Preferences.getInstance().getValue("cura/active_machine") if active_machine_pref: @@ -181,7 +166,6 @@ class CuraApplication(QtApplication): else: self.requestAddPrinter.emit() - self._removableDrivesChanged() if self._engine.rootObjects: self.closeSplash() @@ -401,16 +385,6 @@ class CuraApplication(QtApplication): def expandedCategories(self): return Preferences.getInstance().getValue("cura/categories_expanded").split(";") - outputDevicesChanged = pyqtSignal() - - @pyqtProperty("QVariantMap", notify = outputDevicesChanged) - def outputDevices(self): - return self._output_devices - - @pyqtProperty("QStringList", notify = outputDevicesChanged) - def outputDeviceNames(self): - return self._output_devices.keys() - @pyqtSlot(str, result = "QVariant") def getSettingValue(self, key): if not self.getActiveMachine(): @@ -479,82 +453,6 @@ class CuraApplication(QtApplication): for node in ungrouped_nodes: Selection.remove(node) - ## Add an output device that can be written to. - # - # \param id \type{string} The identifier used to identify the device. - # \param device \type{StorageDevice} A dictionary of device information. - # It should contains the following: - # - function: A function to be called when trying to write to the device. Will be passed the device id as first parameter. - # - description: A translated string containing a description of what happens when writing to the device. - # - icon: The icon to use to represent the device. - # - priority: The priority of the device. The device with the highest priority will be used as the default device. - def addOutputDevice(self, id, device): - self._output_devices[id] = device - self.outputDevicesChanged.emit() - - ## Remove output device - # \param id \type{string} The identifier used to identify the device. - # \sa PrinterApplication::addOutputDevice() - def removeOutputDevice(self, id): - if id in self._output_devices: - del self._output_devices[id] - self.outputDevicesChanged.emit() - - @pyqtSlot(str) - def writeToOutputDevice(self, device): - self._output_devices[device]["function"](device) - - writeToLocalFileRequested = pyqtSignal() - - def _writeToLocalFile(self, device): - self.writeToLocalFileRequested.emit() - - def _writeToSD(self, device): - for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode or not node.getMeshData(): - continue - - try: - path = self.getStorageDevice("LocalFileStorage").getRemovableDrives()[device] - except KeyError: - Logger.log("e", "Tried to write to unknown SD card %s", device) - return - - filename = os.path.join(path, node.getName()[0:node.getName().rfind(".")] + ".gcode") - - message = Message(self._output_devices[device]["description"], 0, False, -1) - message.show() - - job = WriteMeshJob(filename, node.getMeshData()) - job._sdcard = device - job._message = message - job.start() - job.finished.connect(self._onWriteToSDFinished) - - return - - def _removableDrivesChanged(self): - drives = self.getStorageDevice("LocalFileStorage").getRemovableDrives() - for drive in drives: - if drive not in self._output_devices: - self.addOutputDevice(drive, { - "id": drive, - "function": self._writeToSD, - "description": self._i18n_catalog.i18nc("Save button tooltip. {0} is sd card name", "Save to SD Card {0}").format(drive), - "shortDescription": self._i18n_catalog.i18nc("Save button tooltip. {0} is sd card name", "Save to SD Card {0}").format(""), - "icon": "save_sd", - "priority": 1 - }) - - drives_to_remove = [] - for device in self._output_devices: - if device not in drives: - if self._output_devices[device]["function"] == self._writeToSD: - drives_to_remove.append(device) - - for drive in drives_to_remove: - self.removeOutputDevice(drive) - def _onActiveMachineChanged(self): machine = self.getActiveMachine() if machine: @@ -580,25 +478,6 @@ class CuraApplication(QtApplication): else: self._platform.setPosition(Vector(0.0, 0.0, 0.0)) - def _onWriteToSDFinished(self, job): - message = Message(self._i18n_catalog.i18nc("Saved to SD message, {0} is sdcard, {1} is filename", "Saved to SD Card {0} as {1}").format(job._sdcard, job.getFileName())) - message.addAction( - "eject", - self._i18n_catalog.i18nc("Message action", "Eject"), - "eject", - self._i18n_catalog.i18nc("Message action tooltip, {0} is sdcard", "Eject SD Card {0}").format(job._sdcard) - ) - - job._message.hide() - - message._sdcard = job._sdcard - message.actionTriggered.connect(self._onMessageActionTriggered) - message.show() - - def _onMessageActionTriggered(self, message, action): - if action == "eject": - self.getStorageDevice("LocalFileStorage").ejectRemovableDrive(message._sdcard) - def _onFileLoaded(self, job): mesh = job.getResult() if mesh != None: diff --git a/plugins/CuraEngineBackend/__init__.py b/plugins/CuraEngineBackend/__init__.py index 0c9588b1e7..fc986c6f67 100644 --- a/plugins/CuraEngineBackend/__init__.py +++ b/plugins/CuraEngineBackend/__init__.py @@ -9,11 +9,11 @@ catalog = i18nCatalog("cura") def getMetaData(): return { - "type": "backend", "plugin": { "name": "CuraEngine Backend", "author": "Ultimaker", - "description": catalog.i18nc("CuraEngine backend plugin description", "Provides the link to the CuraEngine slicing backend") + "description": catalog.i18nc("CuraEngine backend plugin description", "Provides the link to the CuraEngine slicing backend"), + "api": 2 } } diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index f776c6f0c1..d3db35e762 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -10,18 +10,17 @@ import io class GCodeWriter(MeshWriter): def __init__(self): super().__init__() - self._gcode = None - def write(self, file_name, storage_device, mesh_data): - if "gcode" in file_name: - scene = Application.getInstance().getController().getScene() - gcode_list = getattr(scene, "gcode_list") - if gcode_list: - f = storage_device.openFile(file_name, "wt") - Logger.log("d", "Writing GCode to file %s", file_name) - for gcode in gcode_list: - f.write(gcode) - storage_device.closeFile(f) - return True + def write(self, stream, node, mode = MeshWriter.OutputMode.TextMode): + if mode != MeshWriter.OutputMode.TextMode: + Logger.log("e", "GCode Writer does not support non-text mode") + return False + + scene = Application.getInstance().getController().getScene() + gcode_list = getattr(scene, "gcode_list") + if gcode_list: + for gcode in gcode_list: + stream.write(gcode) + return True return False diff --git a/plugins/GCodeWriter/__init__.py b/plugins/GCodeWriter/__init__.py index 3897cc3f75..ecd63b02b5 100644 --- a/plugins/GCodeWriter/__init__.py +++ b/plugins/GCodeWriter/__init__.py @@ -8,17 +8,21 @@ catalog = i18nCatalog("cura") def getMetaData(): return { - "type": "mesh_writer", "plugin": { "name": "GCode Writer", "author": "Ultimaker", "version": "1.0", - "description": catalog.i18nc("GCode Writer Plugin Description", "Writes GCode to a file") + "description": catalog.i18nc("GCode Writer Plugin Description", "Writes GCode to a file"), + "api": 2 }, "mesh_writer": { - "extension": "gcode", - "description": catalog.i18nc("GCode Writer File Description", "GCode File") + "output": [{ + "extension": "gcode", + "description": catalog.i18nc("GCode Writer File Description", "GCode File"), + "mime_type": "text/x-gcode", + "mode": GCodeWriter.GCodeWriter.OutputMode.TextMode + }] } } diff --git a/plugins/LayerView/__init__.py b/plugins/LayerView/__init__.py index eb3ba4cdbe..4bd9a61fb0 100644 --- a/plugins/LayerView/__init__.py +++ b/plugins/LayerView/__init__.py @@ -9,12 +9,12 @@ catalog = i18nCatalog("cura") def getMetaData(): return { - "type": "view", "plugin": { "name": "Layer View", "author": "Ultimaker", "version": "1.0", - "description": catalog.i18nc("Layer View plugin description", "Provides the Layer view.") + "description": catalog.i18nc("Layer View plugin description", "Provides the Layer view."), + "api": 2 }, "view": { "name": catalog.i18nc("Layers View mode", "Layers"), diff --git a/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py new file mode 100644 index 0000000000..ce948c472b --- /dev/null +++ b/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py @@ -0,0 +1,41 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2013 David Braam +# Uranium is released under the terms of the AGPLv3 or higher. + +from . import RemovableDrivePlugin + +import glob +import os +import subprocess + +## Support for removable devices on Linux. +# +# TODO: This code uses the most basic interfaces for handling this. +# We should instead use UDisks2 to handle mount/unmount and hotplugging events. +# +class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): + def checkRemovableDrives(self): + drives = {} + for volume in glob.glob("/media/*"): + if os.path.ismount(volume): + drives[volume] = os.path.basename(volume) + elif volume == "/media/"+os.getenv("USER"): + for volume in glob.glob("/media/"+os.getenv("USER")+"/*"): + if os.path.ismount(volume): + drives[volume] = os.path.basename(volume) + + for volume in glob.glob("/run/media/" + os.getenv("USER") + "/*"): + if os.path.ismount(volume): + drives[volume] = os.path.basename(volume) + + return drives + + def performEjectDevice(self, device): + p = subprocess.Popen(["umount", device.getId()], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output = p.communicate() + + return_code = p.wait() + if return_code != 0: + return False + else: + return True diff --git a/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py new file mode 100644 index 0000000000..c50443cb92 --- /dev/null +++ b/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py @@ -0,0 +1,64 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2013 David Braam +# Uranium is released under the terms of the AGPLv3 or higher. + +from . import RemovableDrivePlugin + +import threading + +import subprocess +import time +import os + +import plistlib + +## Support for removable devices on Mac OSX +class OSXRemovableDrives(RemovableDrivePlugin.RemovableDrivePlugin): + def run(self): + drives = {} + p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout=subprocess.PIPE) + plist = plistlib.loads(p.communicate()[0]) + p.wait() + + for dev in self._findInTree(plist, "Mass Storage Device"): + if "removable_media" in dev and dev["removable_media"] == "yes" and "volumes" in dev and len(dev["volumes"]) > 0: + for vol in dev["volumes"]: + if "mount_point" in vol: + volume = vol["mount_point"] + drives[volume] = os.path.basename(volume) + + p = subprocess.Popen(["system_profiler", "SPCardReaderDataType", "-xml"], stdout=subprocess.PIPE) + plist = plistlib.loads(p.communicate()[0]) + p.wait() + + for entry in plist: + if "_items" in entry: + for item in entry["_items"]: + for dev in item["_items"]: + if "removable_media" in dev and dev["removable_media"] == "yes" and "volumes" in dev and len(dev["volumes"]) > 0: + for vol in dev["volumes"]: + if "mount_point" in vol: + volume = vol["mount_point"] + drives[volume] = os.path.basename(volume) + + def performEjectDevice(self, device): + p = subprocess.Popen(["diskutil", "eject", path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output = p.communicate() + + return_code = p.wait() + if return_code != 0: + return False + else: + return True + + def _findInTree(self, t, n): + ret = [] + if type(t) is dict: + if "_name" in t and t["_name"] == n: + ret.append(t) + for k, v in t.items(): + ret += self._findInTree(v, n) + if type(t) is list: + for v in t: + ret += self._findInTree(v, n) + return ret diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py new file mode 100644 index 0000000000..2728dfd90b --- /dev/null +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -0,0 +1,87 @@ +import os.path + +from UM.Application import Application +from UM.Logger import Logger +from UM.Message import Message +from UM.Mesh.WriteMeshJob import WriteMeshJob +from UM.Mesh.MeshWriter import MeshWriter +from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator +from UM.OutputDevice.OutputDevice import OutputDevice +from UM.OutputDevice import OutputDeviceError + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("uranium") + +class RemovableDriveOutputDevice(OutputDevice): + def __init__(self, device_id, device_name): + super().__init__(device_id) + + self.setName(device_name) + self.setShortDescription(catalog.i18nc("", "Save to Removable Drive")) + self.setDescription(catalog.i18nc("", "Save to Removable Drive {0}").format(device_name)) + self.setIconName("save_sd") + self.setPriority(1) + + def requestWrite(self, node): + gcode_writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType("text/x-gcode") + if not gcode_writer: + Logger.log("e", "Could not find GCode writer, not writing to removable drive %s", self.getName()) + raise OutputDeviceError.WriteRequestFailedError() + + file_name = None + for n in BreadthFirstIterator(node): + if n.getMeshData(): + file_name = n.getName() + if file_name: + break + + if not file_name: + Logger.log("e", "Could not determine a proper file name when trying to write to %s, aborting", self.getName()) + raise OutputDeviceError.WriteRequestFailedError() + + file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + ".gcode") + + try: + Logger.log("d", "Writing to %s", file_name) + stream = open(file_name, "wt") + job = WriteMeshJob(gcode_writer, stream, node, MeshWriter.OutputMode.TextMode) + job.setFileName(file_name) + job.progress.connect(self._onProgress) + job.finished.connect(self._onFinished) + + message = Message(catalog.i18nc("", "Saving to Removable Drive {0}").format(self.getName()), 0, False, -1) + message.show() + + job._message = message + job.start() + except PermissionError as e: + raise OutputDeviceError.PermissionDeniedError() from e + except OSError as e: + raise OutputDeviceError.WriteRequestFailedError() from e + + def _onProgress(self, job, progress): + if hasattr(job, "_message"): + job._message.setProgress(progress) + self.writeProgress.emit(self, progress) + + def _onFinished(self, job): + if hasattr(job, "_message"): + job._message.hide() + job._message = None + self.writeFinished.emit(self) + if job.getResult(): + message = Message(catalog.i18nc("", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName()))) + message.addAction("eject", catalog.i18nc("", "Eject"), "eject", catalog.i18nc("", "Eject removable device {0}").format(self.getName())) + message.actionTriggered.connect(self._onActionTriggered) + message.show() + self.writeSuccess.emit(self) + else: + message = Message(catalog.i18nc("", "Could not save to removable drive {0}: {1}").format(self.getName(), str(job.getError()))) + message.show() + self.writeError.emit(self) + job.getStream().close() + + def _onActionTriggered(self, message, action): + if action == "eject": + Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self) + diff --git a/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py new file mode 100644 index 0000000000..a4e5e4f3f9 --- /dev/null +++ b/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py @@ -0,0 +1,73 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Uranium is released under the terms of the AGPLv3 or higher. + +import threading +import time + +from UM.Signal import Signal +from UM.Message import Message +from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin + +from . import RemovableDriveOutputDevice + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("uranium") + +class RemovableDrivePlugin(OutputDevicePlugin): + def __init__(self): + super().__init__() + + self._update_thread = threading.Thread(target = self._updateThread) + self._update_thread.setDaemon(True) + + self._check_updates = True + + self._drives = {} + + def start(self): + self._update_thread.start() + + def stop(self): + self._check_updates = False + self._update_thread.join() + + self._addRemoveDrives({}) + + def checkRemovableDrives(self): + raise NotImplementedError() + + def ejectDevice(self, device): + result = self.performEjectDevice(device) + if result: + message = Message(catalog.i18n("Ejected {0}. You can now safely remove the drive.").format(device.getName())) + message.show() + else: + message = Message(catalog.i18n("Failed to eject {0}. Maybe it is still in use?").format(device.getName())) + message.show() + + def performEjectDevice(self, device): + raise NotImplementedError() + + def _updateThread(self): + while self._check_updates: + result = self.checkRemovableDrives() + self._addRemoveDrives(result) + time.sleep(5) + + def _addRemoveDrives(self, drives): + # First, find and add all new or changed keys + for key, value in drives.items(): + if key not in self._drives: + self.getOutputDeviceManager().addOutputDevice(RemovableDriveOutputDevice.RemovableDriveOutputDevice(key, value)) + continue + + if self._drives[key] != value: + self.getOutputDeviceManager().removeOutputDevice(key) + self.getOutputDeviceManager().addOutputDevice(RemovableDriveOutputDevice.RemovableDriveOutputDevice(key, value)) + + # Then check for keys that have been removed + for key in self._drives.keys(): + if key not in drives: + self.getOutputDeviceManager().removeOutputDevice(key) + + self._drives = drives diff --git a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py new file mode 100644 index 0000000000..aa85db0c09 --- /dev/null +++ b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py @@ -0,0 +1,98 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2013 David Braam +# Uranium is released under the terms of the AGPLv3 or higher. + +from . import RemovableDrivePlugin + +import threading +import string + +from ctypes import windll +from ctypes import wintypes + +import ctypes +import time +import os +import subprocess + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("uranium") + +# WinAPI Constants that we need +# Hardcoded here due to stupid WinDLL stuff that does not give us access to these values. +DRIVE_REMOVABLE = 2 + +GENERIC_READ = 2147483648 +GENERIC_WRITE = 1073741824 + +FILE_SHARE_READ = 1 +FILE_SHARE_WRITE = 2 + +IOCTL_STORAGE_EJECT_MEDIA = 2967560 + +OPEN_EXISTING = 3 + +## Removable drive support for windows +class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): + def checkRemovableDrives(self): + drives = {} + + bitmask = windll.kernel32.GetLogicalDrives() + # Check possible drive letters, from A to Z + # Note: using ascii_uppercase because we do not want this to change with locale! + for letter in string.ascii_uppercase: + drive = "{0}:/".format(letter) + + # Do we really want to skip A and B? + # GetDriveTypeA explicitly wants a byte array of type ascii. It will accept a string, but this wont work + if bitmask & 1 and windll.kernel32.GetDriveTypeA(drive.encode("ascii")) == DRIVE_REMOVABLE: + volume_name = "" + name_buffer = ctypes.create_unicode_buffer(1024) + filesystem_buffer = ctypes.create_unicode_buffer(1024) + error = windll.kernel32.GetVolumeInformationW(ctypes.c_wchar_p(drive), name_buffer, ctypes.sizeof(name_buffer), None, None, None, filesystem_buffer, ctypes.sizeof(filesystem_buffer)) + + if error != 0: + volume_name = name_buffer.value + + if not volume_name: + volume_name = catalog.i18nc("Default name for removable device", "Removable Drive") + + # Certain readers will report themselves as a volume even when there is no card inserted, but will show an + # "No volume in drive" warning when trying to call GetDiskFreeSpace. However, they will not report a valid + # filesystem, so we can filter on that. In addition, this excludes other things with filesystems Windows + # does not support. + if filesystem_buffer.value == "": + continue + + # Check for the free space. Some card readers show up as a drive with 0 space free when there is no card inserted. + freeBytes = ctypes.c_longlong(0) + if windll.kernel32.GetDiskFreeSpaceExA(drive.encode("ascii"), ctypes.byref(freeBytes), None, None) == 0: + continue + + if freeBytes.value < 1: + continue + + drives[drive] = "{0} ({1}:)".format(volume_name, letter) + bitmask >>= 1 + + return drives + + def performEjectDevice(self, device): + # Magic WinAPI stuff + # First, open a handle to the Device + handle = windll.kernel32.CreateFileA("\\\\.\\{0}".format(device.getId()[:-1]).encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None ) + + if handle == -1: + print(windll.kernel32.GetLastError()) + return + + result = None + # Then, try and tell it to eject + if not windll.kernel32.DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, None, None, None, None, None, None): + result = False + else: + result = True + + # Finally, close the handle + windll.kernel32.CloseHandle(handle) + return result diff --git a/plugins/RemovableDriveOutputDevice/__init__.py b/plugins/RemovableDriveOutputDevice/__init__.py new file mode 100644 index 0000000000..72ba10f01f --- /dev/null +++ b/plugins/RemovableDriveOutputDevice/__init__.py @@ -0,0 +1,32 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Uranium is released under the terms of the AGPLv3 or higher. + +import platform + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("uranium") + +def getMetaData(): + return { + "plugin": { + "name": catalog.i18nc("Removable Drive Output Device Plugin name", "Removable Drive Output Device Plugin"), + "author": "Ultimaker B.V.", + "description": catalog.i18nc("Removable Drive Output Device Plugin description", "Provides removable drive hotplugging and writing support"), + "version": "1.0", + "api": 2 + } + } + +def register(app): + if platform.system() == "Windows": + from . import WindowsRemovableDrivePlugin + return { "output_device": WindowsRemovableDrivePlugin.WindowsRemovableDrivePlugin() } + elif platform.system() == "Darwin": + from . import OSXRemovableDrivePlugin + return { "output_device": OSXRemovableDrivePlugin.OSXRemovableDrivePlugin() } + elif platform.system() == "Linux": + from . import LinuxRemovableDrivePlugin + return { "output_device": LinuxRemovableDrivePlugin.LinuxRemovableDrivePlugin() } + else: + Logger.log("e", "Unsupported system %s, no removable device hotplugging support available.", platform.system()) + return { } diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 99c61d68e0..f107121f34 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -7,7 +7,7 @@ import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.1 -import UM 1.0 as UM +import UM 1.1 as UM UM.MainWindow { id: base @@ -30,24 +30,52 @@ UM.MainWindow { title: qsTr("&File"); MenuItem { action: actions.open; } - MenuItem { action: actions.save; } + + Menu { + id: recentFilesMenu; + title: "Open Recent" + iconName: "document-open-recent"; + + enabled: Printer.recentFiles.length > 0; + + Instantiator { + model: Printer.recentFiles + MenuItem { + text: { + var path = modelData.toString() + return (index + 1) + ". " + path.slice(path.lastIndexOf("/") + 1); + } + onTriggered: UM.MeshFileHandler.readLocalFile(modelData); + } + onObjectAdded: recentFilesMenu.insertItem(index, object) + onObjectRemoved: recentFilesMenu.removeItem(object) + } + } MenuSeparator { } - Instantiator { - model: Printer.recentFiles - MenuItem { - text: { - var path = modelData.toString() - return (index + 1) + ". " + path.slice(path.lastIndexOf("/") + 1); - } - onTriggered: { - UM.MeshFileHandler.readLocalFile(modelData); - Printer.setPlatformActivity(true) + MenuItem { + text: "Save Selection to File"; + enabled: UM.Selection.hasSelection; + iconName: "document-save-as"; + onTriggered: devicesModel.requestWriteSelectionToDevice("local_file"); + } + Menu { + id: saveAllMenu + title: "Save All" + iconName: "document-save"; + enabled: devicesModel.count > 0 && UM.Backend.progress > 0.99; + + Instantiator { + model: UM.OutputDevicesModel { id: devicesModel; } + + MenuItem { + text: model.description + onTriggered: devicesModel.requestWriteToDevice(model.id); } + onObjectAdded: saveAllMenu.insertItem(index, object) + onObjectRemoved: saveAllMenu.removeItem(object) } - onObjectAdded: fileMenu.insertItem(index, object) - onObjectRemoved: fileMenu.removeItem(object) } MenuSeparator { } @@ -300,7 +328,6 @@ UM.MainWindow { addMachineAction: actions.addMachine; configureMachinesAction: actions.configureMachines; - saveAction: actions.save; } Rectangle { @@ -458,22 +485,6 @@ UM.MainWindow { } } - FileDialog { - id: saveDialog; - //: File save dialog title - title: qsTr("Save File"); - selectExisting: false; - - modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal; - - nameFilters: UM.MeshFileHandler.supportedWriteFileTypes - - onAccepted: - { - UM.MeshFileHandler.writeLocalFile(fileUrl); - } - } - EngineLog { id: engineLog; } @@ -493,7 +504,6 @@ UM.MainWindow { addMachineWizard.visible = true addMachineWizard.printer = false } - onWriteToLocalFileRequested: saveDialog.open(); } Component.onCompleted: UM.Theme.load(UM.Resources.getPath(UM.Resources.ThemesLocation, "cura")) diff --git a/resources/qml/SaveButton.qml b/resources/qml/SaveButton.qml index 004b523336..f5a34b5fc2 100644 --- a/resources/qml/SaveButton.qml +++ b/resources/qml/SaveButton.qml @@ -6,47 +6,18 @@ import QtQuick.Controls 1.1 import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 -import UM 1.0 as UM +import UM 1.1 as UM Rectangle { id: base; - property Action saveAction; - property real progress: UM.Backend.progress; property bool activity: Printer.getPlatformActivity; Behavior on progress { NumberAnimation { duration: 250; } } - property string currentDevice: "local_file" - property bool defaultOverride: false; - property bool defaultAmbiguous: false; - property variant printDuration: PrintInformation.currentPrintTime; property real printMaterialAmount: PrintInformation.materialAmount; - Connections { - target: Printer; - onOutputDevicesChanged: { - if(!base.defaultOverride) { - base.defaultAmbiguous = false; - var device = null; - for(var i in Printer.outputDevices) { - if(device == null) { - device = i; - } else if(Printer.outputDevices[i].priority > Printer.outputDevices[device].priority) { - device = i; - } else if(Printer.outputDevices[i].priority == Printer.outputDevices[device].priority) { - base.defaultAmbiguous = true; - } - } - - if(device != null) { - base.currentDevice = device; - } - } - } - } - Rectangle{ id: background implicitWidth: base.width; @@ -76,7 +47,7 @@ Rectangle { visible: base.progress >= 0 && base.progress < 0.99 ? false : true color: UM.Theme.colors.save_button_estimated_text; font: UM.Theme.fonts.small; - text: + text: { if(base.activity == false) { //: Save button label return qsTr("Please load a 3D model"); @@ -90,6 +61,8 @@ Rectangle { //: Save button label return qsTr("Estimated Print-time"); } + return ""; + } } Label { id: printDurationLabel @@ -113,7 +86,7 @@ Rectangle { elide: mediumLengthDuration ? Text.ElideRight : Text.ElideNone visible: base.activity == false || base.progress < 0.99 ? false : true //: Print material amount save button label - text: base.printMaterialAmount < 0 ? "" : qsTr("%1m material").arg(base.printMaterialAmount); + text: base.printMaterialAmount < 0 ? "" : qsTr("%1m of Material").arg(base.printMaterialAmount); } } Rectangle { @@ -134,29 +107,28 @@ Rectangle { anchors.topMargin: UM.Theme.sizes.save_button_text_margin.height; anchors.left: parent.left anchors.leftMargin: UM.Theme.sizes.default_margin.width; - tooltip: '' + tooltip: devicesModel.activeDevice.description; enabled: progress > 0.99 && base.activity == true width: infoBox.width/6*4.5 height: UM.Theme.sizes.save_button_save_to_button.height + + text: devicesModel.activeDevice.short_description; + style: ButtonStyle { background: Rectangle { color: !control.enabled ? UM.Theme.colors.save_button_inactive : control.hovered ? UM.Theme.colors.save_button_active_hover : UM.Theme.colors.save_button_active; + Label { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter + anchors.centerIn: parent color: UM.Theme.colors.save_button_safe_to_text; font: UM.Theme.fonts.sidebar_save_to; - text: Printer.outputDevices[base.currentDevice].shortDescription; + text: control.text; } } + label: Item { } } - onClicked: - if(base.defaultAmbiguous) { - devicesMenu.popup(); - } else { - Printer.writeToOutputDevice(base.currentDevice); - } + onClicked: devicesModel.requestWriteToDevice(devicesModel.activeDevice.id) } Button { @@ -165,16 +137,20 @@ Rectangle { anchors.topMargin: UM.Theme.sizes.save_button_text_margin.height anchors.right: parent.right anchors.rightMargin: UM.Theme.sizes.default_margin.width; - tooltip: '' + + tooltip: qsTr("Select the active output device"); width: infoBox.width/6*1.3 - UM.Theme.sizes.save_button_text_margin.height; height: UM.Theme.sizes.save_button_save_to_button.height + iconSource: UM.Theme.icons[devicesModel.activeDevice.icon_name]; + style: ButtonStyle { background: Rectangle { - color: UM.Theme.colors.save_button_background; - border.width: control.hovered ? UM.Theme.sizes.save_button_border.width : 0 - border.color: UM.Theme.colors.save_button_border + color: UM.Theme.colors.save_button_background; + border.width: control.hovered ? UM.Theme.sizes.save_button_border.width : 0 + border.color: UM.Theme.colors.save_button_border + Rectangle { id: deviceSelectionIcon color: UM.Theme.colors.save_button_background; @@ -183,14 +159,13 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter; width: parent.height - UM.Theme.sizes.save_button_text_margin.width ; height: parent.height - UM.Theme.sizes.save_button_text_margin.width; + UM.RecolorImage { - anchors.centerIn: parent; - width: parent.width; - height: parent.height; + anchors.fill: parent; sourceSize.width: width; sourceSize.height: height; color: UM.Theme.colors.save_button_active - source: UM.Theme.icons[Printer.outputDevices[base.currentDevice].icon]; + source: control.iconSource; } } Label { @@ -203,24 +178,20 @@ Rectangle { color: UM.Theme.colors.save_button_active; } } + label: Item { } } menu: Menu { id: devicesMenu; Instantiator { - model: Printer.outputDeviceNames; + model: devicesModel; MenuItem { - text: Printer.outputDevices[modelData].description; + text: model.description checkable: true; - checked: base.defaultAmbiguous ? false : modelData == base.currentDevice; + checked: model.id == devicesModel.activeDevice.id; exclusiveGroup: devicesMenuGroup; onTriggered: { - base.defaultOverride = true; - base.currentDevice = modelData; - if(base.defaultAmbiguous) { - base.defaultAmbiguous = false; - Printer.writeToOutputDevice(modelData); - } + devicesModel.setActiveDevice(model.id); } } onObjectAdded: devicesMenu.insertItem(index, object) @@ -230,4 +201,8 @@ Rectangle { } } } -} \ No newline at end of file + + UM.OutputDevicesModel { + id: devicesModel; + } +} diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index 23ddfe4ed7..0c908fc789 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -13,7 +13,6 @@ Rectangle { property Action addMachineAction; property Action configureMachinesAction; - property alias saveAction: saveButton.saveAction; color: UM.Theme.colors.sidebar;