Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
[Feature Request] Pythonista built-in file picker
-
I think Pythonista is really missing something very beneficial in a built-in scriptable file navigator. The one that's inside the "move" dialog is really nice, but only supports folders, and plus, it's not user-accessible.
I think Pythonista should have a
filenav
module that allows for easy creation of nice file navigators. They're pretty hard to write, IMHO, and so could benefit from being built-in. How I envision this is as follows:A
filenav
module that provides file browsers and navigators.- Higher-level functions for picking files with callbacks. A function that would allow for picking files and folders. Something like:
def pick_file(start_dir, folders_only=False, select_multiple=False, should_pick="both")
where
should_pick
is a string that describes what should be selectable,"files"
,"folders"
, or"both"
andfolders_only
is whether the file picker should only display folders, and hide files from view.
2. A subclassable class likefilenav.FilePicker
. This would serve important functions:
- Allow users to implement custom methods for file browsing. Users could define methods for listing directories, etc. A (far from complete) example:class myPicker(filenav.FilePicker): def listdir(dir): '''Should return a list of files and directories below a directory as a dictionary''' list = os.listdir(dir) dirs = [f for f in list if os.path.isdir(os.path.abspath(f))] files = set(list)-set(dirs) return {'dirs':dirs,'files':files} def fileSelected(path): '''Called with the path of a file whenever a file is selected''' pass def dirSelected(path): '''Called with the path of a directory whenever a directory is selected''' pass if __name__=='__main__': a=myPicker('test_dir/a/b/c') #Picked will be a list of both files and directories that the user picked out of test_dir/a/b/c picked = a.pick_file(multiple=True,should_pick='both')
This doesn't immediately seem useful, but has tons of practical applications. For example, it would be easy, using a class like this, to create methods that would make a file browser for zip files. Using
zipfile
, custom methods could be created that allowed browsing through zip files without extracting first. This would allow a pythonista app to let users to pick certain files to extract from a zip.These two functionalities combined could help create a scriptable file browser that's easy for beginners, yet hugely powerful for more advanced users.
I'd very much like to see a built-in file browser with these features in a future release.
P.S. I actually have struggled to create a zip file browser in Pythonista in the past. I've gotten this far on GitHub, and Here's a sideways YouTube video
-
@Webmaster4o This is a great idea. Your listdir() contains an awesome use of sets! It puts my files_and_folders.py to shame! You might want to put the is.listdir() inside a sorted() and using
list
as a variable name will shadow the builtin. -
I think this could be a useful addition to the
dialogs
module. -
I've started working on a file picker dialog that I might add to the
dialogs
module. You can find the code here:→ File Picker.py (works in Pythonista 2 and 3)
The API isn't exactly like you suggested, but similar, and quite extensible. As a demo, I've implemented an alternative "data source" that shows files and folders from an FTP server instead of the local file system. It would easily be possible to build a Zip file browser with this as well...
The idea is basically that you can subclass
TreeNode
to represent whatever kind of tree structure you need. TheTreeDialogController
gets oneTreeNode
as its root and a couple of options (multiple selection etc.). TheTreeNode
is then responsible for loading its children (each of the children is also aTreeNode
), and each node has a title/icon that is shown in the table view. For cases when a node might take a relatively long time to load its children,TreeDialogController
has anasync_mode
option that causes it to automatically dispatch the loading to a background thread, and to show a spinner overlay while loading.Overall, the UI is very close to Pythonista's own directory picker, i.e. I'm using the same icons, table view animations, etc.
-
@omz: Looks awesome. Would this be generic enough that the things in the tree do not actually have to be files? I.e. usable as a generic tree navigation component?
-
@mikael Absolutely, as long as you can represent each item with a title and icon, the data can be pretty much anything. Here's an example of a
TreeNode
subclass that represents the contents of a JSON file (e.g. pyui):import json import sys PY3 = sys.version_info[0] >= 3 if PY3: basestring = str def iteritems(d): if PY3: return d.items() else: return d.iteritems() class JSONTreeNode (TreeNode): def __init__(self, json_path=None, node=None, key=None, level=0): TreeNode.__init__(self) if json_path is not None: with open(json_path, 'rb') as f: root_node = json.load(f) self.node = root_node else: self.node = node if isinstance(self.node, list): self.title = 'list (%i)' % (len(self.node),) self.leaf = False self.icon_name = 'iob:navicon_32' elif isinstance(self.node, dict): self.title = 'dict (%i)' % (len(self.node),) self.leaf = False self.icon_name = 'iob:ios7_folder_outline_32' elif isinstance(self.node, basestring): self.title = '"%s"' % (self.node,) self.leaf = True self.icon_name = 'iob:ios7_information_outline_32' else: self.title = str(self.node) self.leaf = True self.icon_name = 'iob:ios7_information_outline_32' self.level = level if key is not None: self.title = key + ' = ' + self.title elif json_path is not None: self.title = os.path.split(json_path)[1] def expand_children(self): if self.children is not None: self.expanded = True return if isinstance(self.node, list): self.children = [JSONTreeNode(node=n, level=self.level+1) for n in self.node] elif isinstance(self.node, dict): self.children = [JSONTreeNode(node=v, key=k, level=self.level+1) for k, v in iteritems(self.node)] self.expanded = True
Usage:
root_node = JSONTreeNode(os.path.expanduser('~/Documents/Examples/User Interface/Calculator.pyui')) tree_controller = TreeDialogController(root_node) tree_controller.view.present('sheet') tree_controller.view.wait_modal()
And here's how that looks like:
-
@omz Are you still considering adding this to
dialogs
?