From 6e4300b299fdbb5d8911b6b91215e74ee3967d6d Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Wed, 8 Jul 2015 21:26:23 +0200 Subject: [PATCH 01/14] Try to load all plugins, not just plugins with certain metadata --- cura/CuraApplication.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 4cd4831e47..00a36a1348 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -98,16 +98,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): parser.add_argument("file", nargs="*", help="Files to load after starting the application.") From baf4ea9523ac53b7141155bd3681f5baccdaff7e Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Wed, 8 Jul 2015 21:26:54 +0200 Subject: [PATCH 02/14] Use the OutputDeviceModel for selecting output device Replaces the stuff in CuraApplication which really should not be there --- resources/qml/SaveButton.qml | 91 +++++++++++++----------------------- 1 file changed, 32 insertions(+), 59 deletions(-) diff --git a/resources/qml/SaveButton.qml b/resources/qml/SaveButton.qml index 4708995308..8f75c82ec3 100644 --- a/resources/qml/SaveButton.qml +++ b/resources/qml/SaveButton.qml @@ -6,46 +6,17 @@ 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; 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; @@ -113,7 +84,7 @@ Rectangle { elide: mediumLengthDuration ? Text.ElideRight : Text.ElideNone visible: 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 +105,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.currentDevice.description; enabled: progress >= 0.99; width: infoBox.width/6*4.5 height: UM.Theme.sizes.save_button_save_to_button.height + + text: devicesModel.currentDevice.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.requestWriteToCurrentDevice() } Button { @@ -165,16 +135,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.currentDevice.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 +157,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 +176,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.currentDevice.id; exclusiveGroup: devicesMenuGroup; onTriggered: { - base.defaultOverride = true; - base.currentDevice = modelData; - if(base.defaultAmbiguous) { - base.defaultAmbiguous = false; - Printer.writeToOutputDevice(modelData); - } + devicesModel.setCurrentDevice(model.id); } } onObjectAdded: devicesMenu.insertItem(index, object) @@ -230,4 +199,8 @@ Rectangle { } } } -} \ No newline at end of file + + UM.OutputDevicesModel { + id: devicesModel; + } +} From c2e672591c7492531d5dfb140ea9336a2e4e5c78 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Wed, 8 Jul 2015 21:42:20 +0200 Subject: [PATCH 03/14] Remove the output_device related stuff from CuraApplication and fix the qml This is now properly handled by the Output Device API in Uranium --- cura/CuraApplication.py | 117 -------------------------------------- resources/qml/Cura.qml | 61 ++++++++++---------- resources/qml/Sidebar.qml | 1 - 3 files changed, 31 insertions(+), 148 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 00a36a1348..6ca6becabd 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -111,15 +111,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() @@ -159,8 +150,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: @@ -173,7 +162,6 @@ class CuraApplication(QtApplication): else: self.requestAddPrinter.emit() - self._removableDrivesChanged() if self._engine.rootObjects: self.closeSplash() @@ -360,16 +348,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(): @@ -385,82 +363,6 @@ class CuraApplication(QtApplication): self.getActiveMachine().setSettingValueByKey(key, value) - ## 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: @@ -486,25 +388,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/resources/qml/Cura.qml b/resources/qml/Cura.qml index 226f4f59a9..6cb4d0112e 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,21 +30,40 @@ UM.MainWindow { title: qsTr("&File"); MenuItem { action: actions.open; } - MenuItem { action: actions.save; } - MenuSeparator { } + Menu { + id: recentFilesMenu; + title: "Open Recent" - Instantiator { - model: Printer.recentFiles - MenuItem { - text: { - var path = modelData.toString() - return (index + 1) + ". " + path.slice(path.lastIndexOf("/") + 1); + Instantiator { + model: Printer.recentFiles + MenuItem { + text: { + var path = modelData.toString() + return (index + 1) + ". " + path.slice(path.lastIndexOf("/") + 1); + } + onTriggered: UM.MeshFileHandler.readLocalFile(modelData); } - onTriggered: UM.MeshFileHandler.readLocalFile(modelData); + onObjectAdded: recentFilesMenu.insertItem(index, object) + onObjectRemoved: recentFilesMenu.removeItem(object) + } + } + + MenuItem { text: "Save Selection" } + Menu { + id: saveAllMenu + title: "Save All" + + Instantiator { + model: UM.OutputDevicesModel { } + + MenuItem { + text: model.description + onTriggered: model.requestWriteToCurrentDevice(); + } + onObjectAdded: saveAllMenu.insertItem(index, object) + onObjectRemoved: saveAllMenu.removeItem(object) } - onObjectAdded: fileMenu.insertItem(index, object) - onObjectRemoved: fileMenu.removeItem(object) } MenuSeparator { } @@ -278,7 +297,6 @@ UM.MainWindow { addMachineAction: actions.addMachine; configureMachinesAction: actions.configureMachines; - saveAction: actions.save; } Rectangle { @@ -411,22 +429,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; } @@ -442,7 +444,6 @@ UM.MainWindow { Connections { target: Printer onRequestAddPrinter: addMachine.visible = true; - onWriteToLocalFileRequested: saveDialog.open(); } Component.onCompleted: UM.Theme.load(UM.Resources.getPath(UM.Resources.ThemesLocation, "cura")) 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; From 078295d6e1e916197193f715d9c56b4e51e896ee Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Wed, 8 Jul 2015 22:04:50 +0200 Subject: [PATCH 04/14] Write to the right device after changes in Uranium API --- resources/qml/Cura.qml | 4 ++-- resources/qml/SaveButton.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 6cb4d0112e..06401a68d2 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -55,11 +55,11 @@ UM.MainWindow { title: "Save All" Instantiator { - model: UM.OutputDevicesModel { } + model: UM.OutputDevicesModel { id: devicesModel; } MenuItem { text: model.description - onTriggered: model.requestWriteToCurrentDevice(); + onTriggered: devicesModel.requestWriteToDevice(model.id); } onObjectAdded: saveAllMenu.insertItem(index, object) onObjectRemoved: saveAllMenu.removeItem(object) diff --git a/resources/qml/SaveButton.qml b/resources/qml/SaveButton.qml index 8f75c82ec3..214088bf72 100644 --- a/resources/qml/SaveButton.qml +++ b/resources/qml/SaveButton.qml @@ -126,7 +126,7 @@ Rectangle { } label: Item { } } - onClicked: devicesModel.requestWriteToCurrentDevice() + onClicked: devicesModel.requestWriteToDevice(devicesModel.currentDevice.id) } Button { From 2f2cef54a98e2328a638d9bbdfd2e0312606d906 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Thu, 16 Jul 2015 09:26:06 +0200 Subject: [PATCH 05/14] Add mime types to GCodeWriter plugin --- plugins/GCodeWriter/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/GCodeWriter/__init__.py b/plugins/GCodeWriter/__init__.py index 3897cc3f75..65f53f64c3 100644 --- a/plugins/GCodeWriter/__init__.py +++ b/plugins/GCodeWriter/__init__.py @@ -18,7 +18,10 @@ def getMetaData(): "mesh_writer": { "extension": "gcode", - "description": catalog.i18nc("GCode Writer File Description", "GCode File") + "description": catalog.i18nc("GCode Writer File Description", "GCode File"), + "mime_types": [ + "text/x-gcode" + ] } } From d6b3044c797b3a6fa699ee15842c6d02e9973d47 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Tue, 28 Jul 2015 17:57:08 +0200 Subject: [PATCH 06/14] Update GCodeWriter to the new API --- plugins/GCodeWriter/GCodeWriter.py | 23 +++++++++++------------ plugins/GCodeWriter/__init__.py | 11 ++++++++--- 2 files changed, 19 insertions(+), 15 deletions(-) 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..4f0b5dd82a 100644 --- a/plugins/GCodeWriter/__init__.py +++ b/plugins/GCodeWriter/__init__.py @@ -13,12 +13,17 @@ def getMetaData(): "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 + }] } } From 68cde3f7806e130b33eacf2fae4309ed9bae6c82 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Thu, 30 Jul 2015 09:31:28 +0200 Subject: [PATCH 07/14] Update SaveButton to the changed OutputDevicesModel API --- resources/qml/SaveButton.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/qml/SaveButton.qml b/resources/qml/SaveButton.qml index 214088bf72..710fbb5293 100644 --- a/resources/qml/SaveButton.qml +++ b/resources/qml/SaveButton.qml @@ -105,13 +105,13 @@ Rectangle { anchors.topMargin: UM.Theme.sizes.save_button_text_margin.height; anchors.left: parent.left anchors.leftMargin: UM.Theme.sizes.default_margin.width; - tooltip: devicesModel.currentDevice.description; + tooltip: devicesModel.activeDevice.description; enabled: progress >= 0.99; width: infoBox.width/6*4.5 height: UM.Theme.sizes.save_button_save_to_button.height - text: devicesModel.currentDevice.short_description; + text: devicesModel.activeDevice.short_description; style: ButtonStyle { background: Rectangle { @@ -126,7 +126,7 @@ Rectangle { } label: Item { } } - onClicked: devicesModel.requestWriteToDevice(devicesModel.currentDevice.id) + onClicked: devicesModel.requestWriteToDevice(devicesModel.activeDevice.id) } Button { @@ -141,7 +141,7 @@ Rectangle { 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.currentDevice.icon_name]; + iconSource: UM.Theme.icons[devicesModel.activeDevice.icon_name]; style: ButtonStyle { background: Rectangle { @@ -186,10 +186,10 @@ Rectangle { MenuItem { text: model.description checkable: true; - checked: model.id == devicesModel.currentDevice.id; + checked: model.id == devicesModel.activeDevice.id; exclusiveGroup: devicesMenuGroup; onTriggered: { - devicesModel.setCurrentDevice(model.id); + devicesModel.setActiveDevice(model.id); } } onObjectAdded: devicesMenu.insertItem(index, object) From b5e8f01cfab2d0e64d786b53ea0108bfed6fc3cb Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Thu, 30 Jul 2015 16:53:16 +0200 Subject: [PATCH 08/14] Return empty string so we get no errors about assigning undefined to string --- resources/qml/SaveButton.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/qml/SaveButton.qml b/resources/qml/SaveButton.qml index 710fbb5293..83d268fb2b 100644 --- a/resources/qml/SaveButton.qml +++ b/resources/qml/SaveButton.qml @@ -46,7 +46,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.progress < 0) { //: Save button label return qsTr("Please load a 3D model"); @@ -60,6 +60,8 @@ Rectangle { //: Save button label return qsTr("Estimated Print-time"); } + return ""; + } } Label { id: printDurationLabel From db599545215cc34c9c4fae80107dc7d90886e635 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Thu, 30 Jul 2015 17:07:18 +0200 Subject: [PATCH 09/14] Properly implement Save Selection --- resources/qml/Cura.qml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 06401a68d2..02e6738c14 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -49,7 +49,14 @@ UM.MainWindow { } } - MenuItem { text: "Save Selection" } + MenuSeparator { } + + 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" From 619e178f0cb8f737e0507dd7df7ba766c52b4672 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Thu, 30 Jul 2015 17:07:46 +0200 Subject: [PATCH 10/14] Disable recent files if there are no recent files and add an icon --- resources/qml/Cura.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 02e6738c14..7e12b9ba12 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -34,6 +34,9 @@ UM.MainWindow { Menu { id: recentFilesMenu; title: "Open Recent" + iconName: "document-open-recent"; + + enabled: Printer.recentFiles.length > 0; Instantiator { model: Printer.recentFiles From 3e024e1618bd4fb2a1dfc6280030a249515ef6a8 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Thu, 30 Jul 2015 17:08:10 +0200 Subject: [PATCH 11/14] Add an icon for "save all" and only enable the action when it makes sense --- resources/qml/Cura.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 7e12b9ba12..760f96fe62 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -63,6 +63,8 @@ UM.MainWindow { Menu { id: saveAllMenu title: "Save All" + iconName: "document-save"; + enabled: devicesModel.count > 0 && UM.Backend.progress > 0.99; Instantiator { model: UM.OutputDevicesModel { id: devicesModel; } From 825349b47bc9f11b741a71eaae5952d0f7f3b1eb Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Thu, 30 Jul 2015 17:14:22 +0200 Subject: [PATCH 12/14] Add RemovableDrive plugin that has been moved from Uranium Since it now depends on GCodeWriter we should put it somewhere where GCodeWriter actually exists. --- .../LinuxRemovableDrivePlugin.py | 41 ++++++++ .../OSXRemovableDrivePlugin.py | 64 ++++++++++++ .../RemovableDriveOutputDevice.py | 87 ++++++++++++++++ .../RemovableDrivePlugin.py | 73 ++++++++++++++ .../WindowsRemovableDrivePlugin.py | 98 +++++++++++++++++++ .../RemovableDriveOutputDevice/__init__.py | 32 ++++++ 6 files changed, 395 insertions(+) create mode 100644 plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py create mode 100644 plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py create mode 100644 plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py create mode 100644 plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py create mode 100644 plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py create mode 100644 plugins/RemovableDriveOutputDevice/__init__.py 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 { } From b96b069c21083150affb5b73513dfb6f9e468e6f Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Fri, 31 Jul 2015 17:07:06 +0200 Subject: [PATCH 13/14] Remove LocalFileStorage from required plugins and add LocalFileOutputDevice --- cura/CuraApplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 4020d39005..fc810ac76b 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -66,7 +66,7 @@ class CuraApplication(QtApplication): "SelectionTool", "CameraTool", "GCodeWriter", - "LocalFileStorage" + "LocalFileOutputDevice" ]) self._physics = None self._volume = None From a7780d9e42107b9778d55e9cc10ca6d0216fcf2c Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Fri, 31 Jul 2015 17:07:33 +0200 Subject: [PATCH 14/14] Update all plugin metadata to specify API version --- plugins/CuraEngineBackend/__init__.py | 4 ++-- plugins/GCodeWriter/__init__.py | 1 - plugins/LayerView/__init__.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) 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/__init__.py b/plugins/GCodeWriter/__init__.py index 4f0b5dd82a..ecd63b02b5 100644 --- a/plugins/GCodeWriter/__init__.py +++ b/plugins/GCodeWriter/__init__.py @@ -8,7 +8,6 @@ catalog = i18nCatalog("cura") def getMetaData(): return { - "type": "mesh_writer", "plugin": { "name": "GCode Writer", "author": "Ultimaker", 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"),