Browse Source

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
Arjen Hiemstra 9 years ago
parent
commit
1819caaed4

+ 5 - 126
cura/CuraApplication.py

@@ -67,7 +67,7 @@ class CuraApplication(QtApplication):
             "SelectionTool",
             "CameraTool",
             "GCodeWriter",
-            "LocalFileStorage"
+            "LocalFileOutputDevice"
         ])
         self._physics = None
         self._volume = None
@@ -101,16 +101,12 @@ class CuraApplication(QtApplication):
         self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib", "cura"))
         if not hasattr(sys, "frozen"):
             self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
+            self._plugin_registry.loadPlugin("ConsoleLogger")
 
-        self._plugin_registry.loadPlugins({ "type": "logger"})
-        self._plugin_registry.loadPlugins({ "type": "storage_device" })
-        self._plugin_registry.loadPlugins({ "type": "view" })
-        self._plugin_registry.loadPlugins({ "type": "mesh_reader" })
-        self._plugin_registry.loadPlugins({ "type": "mesh_writer" })
-        self._plugin_registry.loadPlugins({ "type": "tool" })
-        self._plugin_registry.loadPlugins({ "type": "extension" })
+        self._plugin_registry.loadPlugins()
 
-        self._plugin_registry.loadPlugin("CuraEngineBackend")
+        if self.getBackend() == None:
+            raise RuntimeError("Could not load the backend plugin!")
 
     def addCommandLineOptions(self, parser):
         super().addCommandLineOptions(parser)
@@ -119,15 +115,6 @@ class CuraApplication(QtApplication):
     def run(self):
         self._i18n_catalog = i18nCatalog("cura");
 
-        self.addOutputDevice("local_file", {
-            "id": "local_file",
-            "function": self._writeToLocalFile,
-            "description": self._i18n_catalog.i18nc("Save button tooltip", "Save to Disk"),
-            "shortDescription": self._i18n_catalog.i18nc("Save button tooltip", "Save to Disk"),
-            "icon": "save",
-            "priority": 0
-        })
-
         self.showSplashMessage(self._i18n_catalog.i18nc("Splash screen message", "Setting up scene..."))
 
         controller = self.getController()
@@ -167,8 +154,6 @@ class CuraApplication(QtApplication):
         self.setMainQml(Resources.getPath(Resources.QmlFilesLocation, "Cura.qml"))
         self.initializeEngine()
 
-        self.getStorageDevice("LocalFileStorage").removableDrivesChanged.connect(self._removableDrivesChanged)
-
         if self.getMachines():
             active_machine_pref = Preferences.getInstance().getValue("cura/active_machine")
             if active_machine_pref:
@@ -181,7 +166,6 @@ class CuraApplication(QtApplication):
         else:
             self.requestAddPrinter.emit()
 
-        self._removableDrivesChanged()
         if self._engine.rootObjects:
             self.closeSplash()
 
@@ -401,16 +385,6 @@ class CuraApplication(QtApplication):
     def expandedCategories(self):
         return Preferences.getInstance().getValue("cura/categories_expanded").split(";")
 
-    outputDevicesChanged = pyqtSignal()
-    
-    @pyqtProperty("QVariantMap", notify = outputDevicesChanged)
-    def outputDevices(self):
-        return self._output_devices
-
-    @pyqtProperty("QStringList", notify = outputDevicesChanged)
-    def outputDeviceNames(self):
-        return self._output_devices.keys()
-
     @pyqtSlot(str, result = "QVariant")
     def getSettingValue(self, key):
         if not self.getActiveMachine():
@@ -479,82 +453,6 @@ class CuraApplication(QtApplication):
         for node in ungrouped_nodes:
             Selection.remove(node)
 
-    ##  Add an output device that can be written to.
-    #
-    #   \param id \type{string} The identifier used to identify the device.
-    #   \param device \type{StorageDevice} A dictionary of device information.
-    #                 It should contains the following:
-    #                 - function: A function to be called when trying to write to the device. Will be passed the device id as first parameter.
-    #                 - description: A translated string containing a description of what happens when writing to the device.
-    #                 - icon: The icon to use to represent the device.
-    #                 - priority: The priority of the device. The device with the highest priority will be used as the default device.
-    def addOutputDevice(self, id, device):
-        self._output_devices[id] = device
-        self.outputDevicesChanged.emit()
-    
-    ##  Remove output device
-    #   \param id \type{string} The identifier used to identify the device.
-    #   \sa PrinterApplication::addOutputDevice()
-    def removeOutputDevice(self, id):
-        if id in self._output_devices:
-            del self._output_devices[id]
-            self.outputDevicesChanged.emit()
-
-    @pyqtSlot(str)
-    def writeToOutputDevice(self, device):
-        self._output_devices[device]["function"](device)
-
-    writeToLocalFileRequested = pyqtSignal()
-    
-    def _writeToLocalFile(self, device):
-        self.writeToLocalFileRequested.emit()
-
-    def _writeToSD(self, device):
-        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
-            if type(node) is not SceneNode or not node.getMeshData():
-                continue
-
-            try:
-                path = self.getStorageDevice("LocalFileStorage").getRemovableDrives()[device]
-            except KeyError:
-                Logger.log("e", "Tried to write to unknown SD card %s", device)
-                return
-    
-            filename = os.path.join(path, node.getName()[0:node.getName().rfind(".")] + ".gcode")
-
-            message = Message(self._output_devices[device]["description"], 0, False, -1)
-            message.show()
-
-            job = WriteMeshJob(filename, node.getMeshData())
-            job._sdcard = device
-            job._message = message
-            job.start()
-            job.finished.connect(self._onWriteToSDFinished)
-
-            return
-
-    def _removableDrivesChanged(self):
-        drives = self.getStorageDevice("LocalFileStorage").getRemovableDrives()
-        for drive in drives:
-            if drive not in self._output_devices:
-                self.addOutputDevice(drive, {
-                    "id": drive,
-                    "function": self._writeToSD,
-                    "description": self._i18n_catalog.i18nc("Save button tooltip. {0} is sd card name", "Save to SD Card {0}").format(drive),
-                    "shortDescription": self._i18n_catalog.i18nc("Save button tooltip. {0} is sd card name", "Save to SD Card {0}").format(""),
-                    "icon": "save_sd",
-                    "priority": 1
-                })
-
-        drives_to_remove = []
-        for device in self._output_devices:
-            if device not in drives:
-                if self._output_devices[device]["function"] == self._writeToSD:
-                    drives_to_remove.append(device)
-
-        for drive in drives_to_remove:
-            self.removeOutputDevice(drive)
-
     def _onActiveMachineChanged(self):
         machine = self.getActiveMachine()
         if machine:
@@ -580,25 +478,6 @@ class CuraApplication(QtApplication):
             else:
                 self._platform.setPosition(Vector(0.0, 0.0, 0.0))
 
-    def _onWriteToSDFinished(self, job):
-        message = Message(self._i18n_catalog.i18nc("Saved to SD message, {0} is sdcard, {1} is filename", "Saved to SD Card {0} as {1}").format(job._sdcard, job.getFileName()))
-        message.addAction(
-            "eject",
-            self._i18n_catalog.i18nc("Message action", "Eject"),
-            "eject",
-            self._i18n_catalog.i18nc("Message action tooltip, {0} is sdcard", "Eject SD Card {0}").format(job._sdcard)
-        )
-
-        job._message.hide()
-
-        message._sdcard = job._sdcard
-        message.actionTriggered.connect(self._onMessageActionTriggered)
-        message.show()
-
-    def _onMessageActionTriggered(self, message, action):
-        if action == "eject":
-            self.getStorageDevice("LocalFileStorage").ejectRemovableDrive(message._sdcard)
-
     def _onFileLoaded(self, job):
         mesh = job.getResult()
         if mesh != None:

+ 2 - 2
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
         }
     }
 

+ 11 - 12
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

+ 8 - 4
plugins/GCodeWriter/__init__.py

@@ -8,17 +8,21 @@ catalog = i18nCatalog("cura")
 
 def getMetaData():
     return {
-        "type": "mesh_writer",
         "plugin": {
             "name": "GCode Writer",
             "author": "Ultimaker",
             "version": "1.0",
-            "description": catalog.i18nc("GCode Writer Plugin Description", "Writes GCode to a file")
+            "description": catalog.i18nc("GCode Writer Plugin Description", "Writes GCode to a file"),
+            "api": 2
         },
 
         "mesh_writer": {
-            "extension": "gcode",
-            "description": catalog.i18nc("GCode Writer File Description", "GCode File")
+            "output": [{
+                "extension": "gcode",
+                "description": catalog.i18nc("GCode Writer File Description", "GCode File"),
+                "mime_type": "text/x-gcode",
+                "mode": GCodeWriter.GCodeWriter.OutputMode.TextMode
+            }]
         }
     }
 

+ 2 - 2
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"),

+ 41 - 0
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

+ 64 - 0
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

+ 87 - 0
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)
+

+ 73 - 0
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

+ 98 - 0
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

Some files were not shown because too many files changed in this diff