mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-06-04 11:14:21 +08:00
Merge branch 'output_device'
* output_device: Update all plugin metadata to specify API version Remove LocalFileStorage from required plugins and add LocalFileOutputDevice Add RemovableDrive plugin that has been moved from Uranium Add an icon for "save all" and only enable the action when it makes sense Disable recent files if there are no recent files and add an icon Properly implement Save Selection Return empty string so we get no errors about assigning undefined to string Update SaveButton to the changed OutputDevicesModel API Update GCodeWriter to the new API Add mime types to GCodeWriter plugin Write to the right device after changes in Uranium API Remove the output_device related stuff from CuraApplication and fix the qml Use the OutputDeviceModel for selecting output device Try to load all plugins, not just plugins with certain metadata
This commit is contained in:
commit
1819caaed4
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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
|
@ -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
|
@ -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)
|
||||
|
73
plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py
Normal file
73
plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py
Normal file
@ -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
|
@ -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
|
32
plugins/RemovableDriveOutputDevice/__init__.py
Normal file
32
plugins/RemovableDriveOutputDevice/__init__.py
Normal file
@ -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 { }
|
@ -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"))
|
||||
|
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.OutputDevicesModel {
|
||||
id: devicesModel;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ Rectangle {
|
||||
|
||||
property Action addMachineAction;
|
||||
property Action configureMachinesAction;
|
||||
property alias saveAction: saveButton.saveAction;
|
||||
|
||||
color: UM.Theme.colors.sidebar;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user