Skip to content

Commit

Permalink
Merge pull request #106 from Davideddu/filechooser
Browse files Browse the repository at this point in the history
Add file chooser facade and support for Linux and Windows
  • Loading branch information
tito committed Mar 10, 2015
2 parents b7c9ddb + c582aac commit 834c6e6
Show file tree
Hide file tree
Showing 6 changed files with 486 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 X
================================== ============= ============= === ======= === =====
6 changes: 5 additions & 1 deletion plyer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)
57 changes: 56 additions & 1 deletion plyer/facades.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -501,3 +501,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)
216 changes: 216 additions & 0 deletions plyer/platforms/linux/filechooser.py
Original file line number Diff line number Diff line change
@@ -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()
Loading

0 comments on commit 834c6e6

Please sign in to comment.