From e7d234e72c2a67b20262a9a2ad38549b2975e195 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Mon, 19 Jan 2015 21:31:19 +0100 Subject: [PATCH 1/4] add filechooser facade and support for Linux and Windows --- README.rst | 1 + plyer/__init__.py | 6 +- plyer/facades.py | 57 ++++++- plyer/platforms/linux/filechooser.py | 216 +++++++++++++++++++++++++++ plyer/platforms/win/filechooser.py | 102 +++++++++++++ 5 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 plyer/platforms/linux/filechooser.py create mode 100644 plyer/platforms/win/filechooser.py diff --git a/README.rst b/README.rst index b82117436..f3985ad95 100644 --- a/README.rst +++ b/README.rst @@ -32,4 +32,5 @@ Compass X X X Unique ID (IMEI or SN) X X X X X X Gyroscope X X X Battery X X X X X X +Native file chooser X X ================================== ============= ============= === ======= === ===== diff --git a/plyer/__init__.py b/plyer/__init__.py index f962e2213..aadc14df9 100644 --- a/plyer/__init__.py +++ b/plyer/__init__.py @@ -6,7 +6,7 @@ __all__ = ('accelerometer', 'camera', 'gps', 'notification', 'tts', 'email', 'vibrator', 'sms', 'compass', - 'gyroscope', 'uniqueid', 'battery') + 'gyroscope', 'uniqueid', 'battery', 'irblaster', 'filechooser') __version__ = '1.2.4-dev' @@ -64,3 +64,7 @@ #: IrBlaster proxy to :class:`plyer.facades.IrBlaster` irblaster = Proxy( 'irblaster', facades.IrBlaster) + +#: FileChooser proxy to :class:`plyer.facades.FileChooser` +filechooser = Proxy( + 'filechooser', facades.FileChooser) diff --git a/plyer/facades.py b/plyer/facades.py index b175a3f73..241df614c 100644 --- a/plyer/facades.py +++ b/plyer/facades.py @@ -8,7 +8,7 @@ __all__ = ('Accelerometer', 'Camera', 'GPS', 'Notification', 'TTS', 'Email', 'Vibrator', 'Sms', 'Compass', - 'Gyroscope', 'UniqueID', 'Battery', 'IrBlaster') + 'Gyroscope', 'UniqueID', 'Battery', 'IrBlaster', 'FileChooser') class Accelerometer(object): @@ -483,3 +483,58 @@ def exists(self): def _exists(self): raise NotImplementedError() + + +class FileChooser(object): + '''Native filechooser dialog facade. + + open_file, save_file and choose_dir accept a number of arguments + listed below. They return either a list of paths (normally + absolute), or None if no file was selected or the operation was + canceled and no result is available. + + Arguments: + * **path** *(string or None)*: a path that will be selected + by default, or None + * **multiple** *(bool)*: True if you want the dialog to + allow multiple file selection. (Note: Windows doesn't + support multiple directory selection) + * **filters** *(iterable)*: either a list of wildcard patterns + or of sequences that contain the name of the filter and any + number of wildcards that will be grouped under that name + (e.g. [["Music", "*mp3", "*ogg", "*aac"], "*jpg", "*py"]) + * **preview** *(bool)*: True if you want the file chooser to + show a preview of the selected file, if supported by the + back-end. + * **title** *(string or None)*: The title of the file chooser + window, or None for the default title. + * **icon** *(string or None)*: Path to the icon of the file + chooser window (where supported), or None for the back-end's + default. + * **show_hidden** *(bool)*: Force showing hidden files (currently + supported only on Windows) + + Important: these methods will return only after user interaction. + Use threads or you will stop the mainloop if your app has one. + ''' + + def _file_selection_dialog(self, **kwargs): + raise NotImplementedError() + + def open_file(self, *args, **kwargs): + """Open the file chooser in "open" mode. + """ + return self._file_selection_dialog(mode="open", *args, **kwargs) + + def save_file(self, *args, **kwargs): + """Open the file chooser in "save" mode. Confirmation will be asked + when a file with the same name already exists. + """ + return self._file_selection_dialog(mode="save", *args, **kwargs) + + def choose_dir(self, *args, **kwargs): + """Open the directory chooser. Note that on Windows this is very + limited. Consider writing your own chooser if you target that + platform and are planning on using unsupported features. + """ + return self._file_selection_dialog(mode="dir", *args, **kwargs) \ No newline at end of file diff --git a/plyer/platforms/linux/filechooser.py b/plyer/platforms/linux/filechooser.py new file mode 100644 index 000000000..8b44ad384 --- /dev/null +++ b/plyer/platforms/linux/filechooser.py @@ -0,0 +1,216 @@ +''' +Linux file chooser +------------------ +''' + +from plyer.facades import FileChooser +from distutils.spawn import find_executable as which +import os +import subprocess as sp +import time + + +class SubprocessFileChooser(object): + """A file chooser implementation that allows using + subprocess back-ends. + Normally you only need to override _gen_cmdline, executable, + separator and successretcode. + """ + + executable = "" + """The name of the executable of the back-end. + """ + + separator = "|" + """The separator used by the back-end. Override this for automatic + splitting, or override _split_output. + """ + + successretcode = 0 + """The return code which is returned when the user doesn't close the + dialog without choosing anything, or when the app doesn't crash. + """ + + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + _process = None + + def _run_command(self, cmd): + self._process = sp.Popen(cmd, stdout=sp.PIPE) + while True: + ret = self._process.poll() + if ret != None: + if ret == self.successretcode: + out = self._process.communicate()[0].strip() + self.selection = self._split_output(out) + return self.selection + else: + return None + time.sleep(0.1) + + def _split_output(self, out): + """This methods receives the output of the back-end and turns + it into a list of paths. + """ + return out.split(self.separator) + + def _gen_cmdline(self): + """Returns the command line of the back-end, based on the current + properties. You need to override this. + """ + raise NotImplementedError() + + def run(self): + return self._run_command(self._gen_cmdline()) + + +class ZenityFileChooser(SubprocessFileChooser): + """A FileChooser implementation using Zenity (on GNU/Linux). + + Not implemented features: + * show_hidden + * preview + """ + + executable = "zenity" + separator = "|" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [which(self.executable), "--file-selection", "--confirm-overwrite"] + if self.multiple: + cmdline += ["--multiple"] + if self.mode == "save": + cmdline += ["--save"] + elif self.mode == "dir": + cmdline += ["--directory"] + if self.path: + cmdline += ["--filename", self.path] + if self.title: + cmdline += ["--name", self.title] + if self.icon: + cmdline += ["--window-icon", self.icon] + for f in self.filters: + if type(f) == str: + cmdline += ["--file-filter", f] + else: + cmdline += ["--file-filter", "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:]))] + return cmdline + +class KDialogFileChooser(SubprocessFileChooser): + """A FileChooser implementation using KDialog (on GNU/Linux). + + Not implemented features: + * show_hidden + * preview + """ + + executable = "kdialog" + separator = "\n" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [which(self.executable)] + + filt = [] + + for f in self.filters: + if type(f) == str: + filt += [f] + else: + filt += list(f[1:]) + + if self.mode == "dir": + cmdline += ["--getexistingdirectory", (self.path if self.path else os.path.expanduser("~"))] + elif self.mode == "save": + cmdline += ["--getopenfilename", (self.path if self.path else os.path.expanduser("~")), " ".join(filt)] + else: + cmdline += ["--getopenfilename", (self.path if self.path else os.path.expanduser("~")), " ".join(filt)] + if self.multiple: + cmdline += ["--multiple", "--separate-output"] + if self.title: + cmdline += ["--title", self.title] + if self.icon: + cmdline += ["--icon", self.icon] + return cmdline + +class YADFileChooser(SubprocessFileChooser): + """A NativeFileChooser implementation using YAD (on GNU/Linux). + + Not implemented features: + * show_hidden + """ + + executable = "yad" + separator = "|?|" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [which(self.executable), "--file-selection", "--confirm-overwrite", "--geometry", "800x600+150+150"] + if self.multiple: + cmdline += ["--multiple", "--separator", self.separator] + if self.mode == "save": + cmdline += ["--save"] + elif self.mode == "dir": + cmdline += ["--directory"] + if self.preview: + cmdline += ["--add-preview"] + if self.path: + cmdline += ["--filename", self.path] + if self.title: + cmdline += ["--name", self.title] + if self.icon: + cmdline += ["--window-icon", self.icon] + for f in self.filters: + if type(f) == str: + cmdline += ["--file-filter", f] + else: + cmdline += ["--file-filter", "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:]))] + return cmdline + +CHOOSERS = {"gnome": ZenityFileChooser, + "kde": KDialogFileChooser, + "yad": YADFileChooser} + +class LinuxFileChooser(FileChooser): + """FileChooser implementation for GNu/Linux. Accepts one additional + keyword argument, *desktop_override*, which, if set, overrides the + back-end that will be used. Set it to "gnome" for Zenity, to "kde" + for KDialog and to "yad" for YAD (Yet Another Dialog). + If set to None or not set, a default one will be picked based on + the running desktop environment and installed back-ends. + """ + + desktop = None + if str(os.environ.get("XDG_CURRENT_DESKTOP")).lower() == "kde" and which("kdialog"): + desktop = "kde" + elif which("yad"): + desktop = "yad" + elif which("zenity"): + desktop = "gnome" + + def _file_selection_dialog(self, desktop_override=desktop, **kwargs): + if not desktop_override: + desktop_override = desktop + # This means we couldn't find any back-end + if not desktop_override: + raise OSError("No back-end available. Please install one.") + + chooser = CHOOSERS[desktop_override] + c = chooser(**kwargs) + return c.run() + + +def instance(): + return LinuxFileChooser() diff --git a/plyer/platforms/win/filechooser.py b/plyer/platforms/win/filechooser.py new file mode 100644 index 000000000..2e19d619c --- /dev/null +++ b/plyer/platforms/win/filechooser.py @@ -0,0 +1,102 @@ +''' +Windows file chooser +-------------------- +''' + +from plyer.facades import FileChooser +from win32com.shell import shell, shellcon +import os +import win32gui, win32con, pywintypes + + +class Win32FileChooser(NativeFileChooserBase): + """A native implementation of NativeFileChooser using the + Win32 API on Windows. + + Not Implemented features (all dialogs): + * preview + * icon + + Not implemented features (in directory selection only - it's limited + by Windows itself): + * preview + * window-icon + * multiple + * show_hidden + * filters + * path + """ + + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + def run(self): + try: + if mode != "dir": + args = {} + + if self.path: + atgs["InitialDir"] = os.path.dirname(self.path) + args["File"] = os.path.splitext(os.path.dirname(self.path))[0] + args["DefExt"] = os.path.splitext(os.path.dirname(self.path))[1] + args["Title"] = self.title if self.title else "Pick a file..." + args["CustomFilter"] = 'Other file types\x00*.*\x00' + args["FilterIndex"] = 1 + + filters = "" + for f in self.filters: + if type(f) == str: + filters += (f + "\x00") * 2 + else: + filters += f[0] + "\x00" + ";".join(f[1:]) + "\x00" + args["Filter"] = filters + + flags = win32con.OFN_EXTENSIONDIFFERENT | win32con.OFN_OVERWRITEPROMPT + if self.multiple: + flags |= win32con.OFN_ALLOWmultiple | win32con.OFN_EXPLORER + if self.show_hidden: + flags |= win32con.OFN_FORCESHOWHIDDEN + args["Flags"] = flags + + if self.mode == "open": + self.fname, self.customfilter, self.flags = win32gui.GetOpenFileNameW(**args) + elif self.mode == "save": + self.fname, self.customfilter, self.flags = win32gui.GetSaveFileNameW(**args) + + if self.fname: + if self.multiple: + seq = str(self.fname).split("\x00") + dir_n, base_n = seq[0], seq[1:] + self.selection = [os.path.join(dir_n, i) for i in base_n] + else: + self.selection = str(self.fname).split("\x00") + else: + # From http://timgolden.me.uk/python/win32_how_do_i/browse-for-a-folder.html + pidl, display_name, image_list = shell.SHBrowseForFolder( + win32gui.GetDesktopWindow(), None, + self.title if self.title else "Pick a folder...", 0, None, None) + self.selection = [str(shell.SHGetPathFromIDList (pidl))] + + return self.selection + except (RuntimeError, pywintypes.error): + return None + +class WinFileChooser(FileChooser): + """FileChooser implementation for Windows, using win3all. + """ + def _file_selection_dialog(self, **kwargs): + return Win32FileChooser(**kwargs).run() + + +def instance(): + return WinFileChooser() From 217eab7d7be08c3fa5346c8a50a7287bed4f47d9 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Mon, 19 Jan 2015 21:39:17 +0100 Subject: [PATCH 2/4] fix inheritance issue on windows filechooser implementation --- plyer/platforms/win/filechooser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plyer/platforms/win/filechooser.py b/plyer/platforms/win/filechooser.py index 2e19d619c..80cd43189 100644 --- a/plyer/platforms/win/filechooser.py +++ b/plyer/platforms/win/filechooser.py @@ -9,7 +9,7 @@ import win32gui, win32con, pywintypes -class Win32FileChooser(NativeFileChooserBase): +class Win32FileChooser(object): """A native implementation of NativeFileChooser using the Win32 API on Windows. From 1b6320a3028059623d52669137cdf692ae58d425 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Mon, 19 Jan 2015 22:38:06 +0100 Subject: [PATCH 3/4] add experimental support for Mac OS X --- README.rst | 2 +- plyer/platforms/macosx/filechooser.py | 98 +++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 plyer/platforms/macosx/filechooser.py diff --git a/README.rst b/README.rst index f3985ad95..a02ec19a7 100644 --- a/README.rst +++ b/README.rst @@ -32,5 +32,5 @@ Compass X X X Unique ID (IMEI or SN) X X X X X X Gyroscope X X X Battery X X X X X X -Native file chooser X X +Native file chooser X X X ================================== ============= ============= === ======= === ===== diff --git a/plyer/platforms/macosx/filechooser.py b/plyer/platforms/macosx/filechooser.py new file mode 100644 index 000000000..9e25221cd --- /dev/null +++ b/plyer/platforms/macosx/filechooser.py @@ -0,0 +1,98 @@ +''' +Mac OS X file chooser +--------------------- +''' + +from plyer.facades import FileChooser +from pyobjus import autoclass, objc_arr, objc_str +from pyobjus.dylib_manager import load_framework, INCLUDE + +load_framework(INCLUDE.AppKit) +NSURL = autoclass('NSURL') +NSOpenPanel = autoclass('NSOpenPanel') +NSSavePanel = autoclass('NSSavePanel') +NSOKButton = 1 + +class MacFileChooser(object): + """A native implementation of file chooser dialogs using Apple's API + through pyobjus. + + Not implemented features: + * filters (partial, wildcards are converted to extensions if possible. + Pass the Mac-specific "use_extensions" if you can provide + Mac OS X-compatible to avoid automatic conversion) + * multiple (only for save dialog. Available in open dialog) + * icon + * preview + """ + + mode = "open" + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + use_extensions = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + def run(self): + panel = None + if self.mode in ("open", "dir"): + panel = NSOpenPanel.openPanel() + else: + panel = NSSavePanel.savePanel() + + panel.setCanCreateDirectories_(True) + + panel.setCanChooseDirectories_(self.mode == "dir") + panel.setCanChooseFiles_(self.mode != "dir") + panel.setShowsHiddenFiles_(self.show_hidden) + + if self.title: + panel.setTitle_(objc_str(self.title)) + + if self.mode != "save" and self.multiple: + panel.setAllowsMultipleSelection_(True) + + # Mac OS X does not support wildcards unlike the other platforms. + # This tries to convert wildcards to "extensions" when possible, + # ans sets the panel to also allow other file types, just to be safe. + if len(self.filters) > 0 + filthies = [] + for f in self.filters: + if not self.use_extensions: + # This would lead to an empty filter, no extension can be obtained + if f.strip().endswith("*"): + continue + pystr = f.strip().split("*")[-1].split(".")[-1] + filthies.append(objc_str(pystr)) + ftypes_arr = objc_arr(filthies) + panel.setAllowedFileTypes_(ftypes) + panel.setAllowsOtherFileTypes_(not self.use_extensions) + + if self.path: + url = NSURL.fileURLWithPath_(self.path) + panel.setDirectoryURL_(url) + + if panel.runModal_(): + if self.mode == "save" or not self.multiple: + return [panel.filename().UTF8String()] + else: + return [i.UTF8String() for i in panel.filenames()] + return None + + +class MacOSXFileChooser(FileChooser): + """FileChooser implementation for Windows, using win3all. + """ + def _file_selection_dialog(self, **kwargs): + return MacFileChooser(**kwargs).run() + +def instance(): + return MacOSXFileChooser() From c582aac1bc607517843a8144916a7b2af92deb26 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Mon, 19 Jan 2015 22:44:13 +0100 Subject: [PATCH 4/4] fix filter conversion on Mac OS X --- plyer/platforms/macosx/filechooser.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plyer/platforms/macosx/filechooser.py b/plyer/platforms/macosx/filechooser.py index 9e25221cd..520d32711 100644 --- a/plyer/platforms/macosx/filechooser.py +++ b/plyer/platforms/macosx/filechooser.py @@ -66,12 +66,20 @@ def run(self): if len(self.filters) > 0 filthies = [] for f in self.filters: - if not self.use_extensions: - # This would lead to an empty filter, no extension can be obtained - if f.strip().endswith("*"): - continue - pystr = f.strip().split("*")[-1].split(".")[-1] - filthies.append(objc_str(pystr)) + if type(f) == str: + if not self.use_extensions: + if f.strip().endswith("*"): + continue + pystr = f.strip().split("*")[-1].split(".")[-1] + filthies.append(objc_str(pystr)) + else: + for i in f[1:]: + if not self.use_extensions: + if f.strip().endswith("*"): + continue + pystr = f.strip().split("*")[-1].split(".")[-1] + filthies.append(objc_str(pystr)) + ftypes_arr = objc_arr(filthies) panel.setAllowedFileTypes_(ftypes) panel.setAllowsOtherFileTypes_(not self.use_extensions)