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:
Arjen Hiemstra 2015-07-31 17:37:25 +02:00
commit 1819caaed4
14 changed files with 500 additions and 239 deletions

View File

@ -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:

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}]
}
}

View File

@ -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"),

View File

@ -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

View File

@ -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

View File

@ -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)

View 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

View File

@ -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

View 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 { }

View File

@ -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"))

View File

@ -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;
}
}

View File

@ -13,7 +13,6 @@ Rectangle {
property Action addMachineAction;
property Action configureMachinesAction;
property alias saveAction: saveButton.saveAction;
color: UM.Theme.colors.sidebar;