commit 990e2cb627e1ebaeb0ddff687116368eda247fa7 Author: Thomas Bouve Date: Thu Apr 11 16:18:17 2019 +0200 First public release diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..0b19786 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +#Ignore vscode individual settings +.vscode + +#Ignore egg-info files and folders +*.egg-info +*.pyc + +#Ignore pyenv version files +.python-version diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3b4030f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Thomas Bouve + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d262f5 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# ipyfilechooser + +A simple Python file chooser widget for use in Jupyter/IPython in conjunction with ipywidgets. The selected path and file are available via `.selected_path` and `.selected_filename` respectvely or as a single combined filepath via `selected`. The dialog can be reset to its default path and filename by using `.reset()`. + +If a filename is typed in the filename text field that matches a file entry in the current folder the entry will be highlighted. To highlight the risk of overwriting existing files, the selected filepath will be green if the file does not exist and orange if it does. + +## Usage + +``` +from ipyfilechooser import FileChooser + +# Create and display a FileChooser widget +fc = FileChooser('/Users/crahan/Projects/Jupyter/ipywidgets') +display(fc) + +# Print the selected path, filename, or both +print(fc.selected_path) +print(fc.selected_filename) +print(fc.selected) + +# Change defaults and reset +fc.default_path = '/Users/crahan/Projects' +fc.default_filename = 'output.txt' +fc.reset() + +# Shorthand reset +fc.reset(path='/Users/crahan/Projects', filename='output.txt') +``` + +## Functions and variables + +``` +fc.reset() +fc.default +fc.default_path +fc.default_filepath +fc.selected +fc.selected_path +fc.selected_filename +``` + +## Screenshots + +![Screenshot 1](screenshots/FileChooser_screenshot_1.png) +*Fig. 1: FileChooser default view* + +![Screenshot 2](screenshots/FileChooser_screenshot_2.png) +*Fig. 2: FileChooser open view* + +![Screenshot 3](screenshots/FileChooser_screenshot_3.png) +*Fig. 3: Entering a filename for an existing file* + +![Screenshot 4](screenshots/FileChooser_screenshot_4.png) +*Fig. 4: Existing file selected* + +![Screenshot 5](screenshots/FileChooser_screenshot_5.png) +*Fig. 4: Entering a filename for a new file* + +![Screenshot 6](screenshots/FileChooser_screenshot_6.png) +*Fig. 5: New file selected* + +![Screenshot 7](screenshots/FileChooser_screenshot_7.png) +*Fig. 5: Quick navigation dropdown* \ No newline at end of file diff --git a/ipyfilechooser/__init__.py b/ipyfilechooser/__init__.py new file mode 100755 index 0000000..79ed1cf --- /dev/null +++ b/ipyfilechooser/__init__.py @@ -0,0 +1,400 @@ +from ipywidgets import Dropdown, Text, Select, Button, HTML +from ipywidgets import Layout, GridBox, HBox, VBox +import os + + +class FileChooser(VBox): + + _LBL_TEMPLATE = '{0}' + _LBL_NOFILE = 'No file selected' + + def __init__(self, path=os.getcwd(), filename='', **kwargs): + + self._default_path = path.rstrip(os.path.sep) + self._default_filename = filename + self._selected_path = '' + self._selected_filename = '' + + # Widgets + self._pathlist = Dropdown( + description="", + layout=Layout( + width='auto', + grid_area='pathlist' + ) + ) + self._filename = Text( + placeholder='output filename', + layout=Layout( + width='auto', + grid_area='filename' + ) + ) + self._dircontent = Select( + rows=8, + layout=Layout( + width='auto', + grid_area='dircontent' + ) + ) + self._cancel = Button( + description='Cancel', + layout=Layout( + width='auto', + display='none' + ) + ) + self._select = Button( + description='Select', + layout=Layout(width='auto') + ) + + # Widget observe handlers + self._pathlist.observe( + self._on_pathlist_select, + names='value' + ) + self._dircontent.observe( + self._on_dircontent_select, + names='value' + ) + self._filename.observe( + self._on_filename_change, + names='value' + ) + self._select.on_click(self._on_select_click) + self._cancel.on_click(self._on_cancel_click) + + # Selected file label + self._label = HTML( + value=self._LBL_TEMPLATE.format( + self._LBL_NOFILE, + 'black' + ), + placeholder='', + description='' + ) + + # Layout + self._gb = GridBox( + children=[ + self._pathlist, + self._filename, + self._dircontent + ], + layout=Layout( + display='none', + width='500px', + grid_gap='0px 0px', + grid_template_rows='auto auto', + grid_template_columns='60% 40%', + grid_template_areas=''' + 'pathlist filename' + 'dircontent dircontent' + ''' + ) + ) + buttonbar = HBox( + children=[ + self._select, + self._cancel, + self._label + ], + layout=Layout(width='auto') + ) + + # Call setter to set initial form values + self._set_form_values( + self._default_path, + self._default_filename + ) + + # Call VBox super class __init__ + super().__init__( + children=[ + self._gb, + buttonbar, + ], + layout=Layout(width='auto'), + **kwargs + ) + + def _get_subpaths(self, path): + '''Walk a path and return a list of subpaths''' + if os.path.isfile(path): + path = os.path.dirname(path) + + paths = [path] + path, tail = os.path.split(path) + + while tail: + paths.append(path) + path, tail = os.path.split(path) + + return paths + + def _update_path(self, path, item): + '''Update path with new item''' + if item == '..': + path = os.path.dirname(path) + else: + path = os.path.join(path, item) + + return path + + def _has_parent(self, path): + '''Check if a path has a parent folder''' + return os.path.basename(path) != '' + + def _get_dir_contents(self, path, showhidden=False): + '''Get directory contents''' + files = list() + dirs = list() + + if os.path.isdir(path): + for item in os.listdir(path): + append = True + if item.startswith('.') and not showhidden: + append = False + full_item = os.path.join(path, item) + if os.path.isdir(full_item) and append: + dirs.append(item) + elif append: + files.append(item) + if self._has_parent(path): + dirs.insert(0, '..') + return sorted(dirs) + sorted(files) + + def _set_form_values(self, path, filename): + '''Set the form values''' + + # Disable triggers to prevent selecting an entry in the Select + # box from automatically triggering a new event. + self._pathlist.unobserve( + self._on_pathlist_select, + names='value' + ) + self._dircontent.unobserve( + self._on_dircontent_select, + names='value' + ) + self._filename.unobserve( + self._on_filename_change, + names='value' + ) + + # Set form values + self._pathlist.options = self._get_subpaths(path) + self._pathlist.value = path + self._dircontent.options = self._get_dir_contents(path) + self._filename.value = filename + + # If the value in the filename Text box equals a value in the + # Select box and the entry is a file then select the entry. + if ((filename in self._dircontent.options) and + os.path.isfile(os.path.join(path, filename))): + self._dircontent.value = filename + else: + self._dircontent.value = None + + # Reenable triggers again + self._pathlist.observe( + self._on_pathlist_select, + names='value' + ) + self._dircontent.observe( + self._on_dircontent_select, + names='value' + ) + self._filename.observe( + self._on_filename_change, + names='value' + ) + + # Set the state of the select Button + if self._gb.layout.display is None: + selected = os.path.join( + self._selected_path, + self._selected_filename + ) + + # filename value is empty or equals the selected value + if (filename == '') or (os.path.join(path, filename) == selected): + self._select.disabled = True + else: + self._select.disabled = False + + def _on_pathlist_select(self, change): + '''Handler for when a new path is selected''' + self._set_form_values( + change['new'], + self._filename.value + ) + + def _on_dircontent_select(self, change): + '''Handler for when a folder entry is selected''' + new_path = self._update_path( + self._pathlist.value, + change['new'] + ) + + # Check if folder or file + if os.path.isdir(new_path): + path = new_path + filename = self._filename.value + elif os.path.isfile(new_path): + path = self._pathlist.value + filename = change['new'] + + self._set_form_values( + path, + filename + ) + + def _on_filename_change(self, change): + '''Handler for when the filename field changes''' + self._set_form_values( + self._pathlist.value, + change['new'] + ) + + def _on_select_click(self, b): + '''Handler for when the select button is clicked''' + if self._gb.layout.display is 'none': + self._gb.layout.display = None + self._cancel.layout.display = None + + # Show the form with the correct path and filename + if self._selected_path and self._selected_filename: + path = self._selected_path + filename = self._selected_filename + else: + path = self._default_path + filename = self._default_filename + + self._set_form_values(path, filename) + + else: + self._gb.layout.display = 'none' + self._cancel.layout.display = 'none' + self._select.description = 'Change' + self._selected_path = self._pathlist.value + self._selected_filename = self._filename.value + # self._default_path = self._selected_path + # self._default_filename = self._selected_filename + + selected = os.path.join( + self._selected_path, + self._selected_filename + ) + + if os.path.isfile(selected): + self._label.value = self._LBL_TEMPLATE.format( + selected, + 'orange' + ) + else: + self._label.value = self._LBL_TEMPLATE.format( + selected, + 'green' + ) + + def _on_cancel_click(self, b): + '''Handler for when the cancel button is clicked''' + self._gb.layout.display = 'none' + self._cancel.layout.display = 'none' + self._select.disabled = False + + def reset(self, path=None, filename=None): + '''Reset the form to the default path and filename''' + self._selected_path = '' + self._selected_filename = '' + + self._label.value = self._LBL_TEMPLATE.format( + self._LBL_NOFILE, + 'black' + ) + + if path is not None: + self._default_path = path.rstrip(os.path.sep) + + if filename is not None: + self._default_filename = filename + + self._set_form_values( + self._default_path, + self._default_filename + ) + + @property + def selected(self): + '''Get selected value''' + return os.path.join( + self._selected_path, + self._selected_filename + ) + + @property + def selected_path(self): + '''Get selected_path value''' + return self._selected_path + + @property + def selected_filename(self): + '''Get the selected_filename''' + return self._selected_filename + + @property + def default(self): + '''Get the default value''' + return os.path.join( + self._default_path, + self._default_filename + ) + + @property + def default_path(self): + '''Get the default_path value''' + return self._default_path + + @property + def default_filename(self): + '''Get the default_filename value''' + return self._default_filename + + @default_path.setter + def default_path(self, path): + '''Set the default_path''' + self._default_path = path.rstrip(os.path.sep) + self._default = os.path.join( + self._default_path, + self._filename.value + ) + self._set_form_values( + self._default_path, + self._filename.value + ) + + @default_filename.setter + def default_filename(self, filename): + '''Set the default_filename''' + self._default_filename = filename + self._default = os.path.join( + self._pathlist.value, + self._default_filename + ) + self._set_form_values( + self._pathlist.value, + self._default_filename + ) + + def __repr__(self): + str_ = "FileChooser(path='{0}', filename='{1}')".format( + self._default_path, + self._default_filename + ) + return str_ + +# Todo +# - keep generic functions into __init__.py +# - move the class to FileChooser.py (allowed?) diff --git a/screenshots/FileChooser_screenshot_1.png b/screenshots/FileChooser_screenshot_1.png new file mode 100644 index 0000000..617cddf Binary files /dev/null and b/screenshots/FileChooser_screenshot_1.png differ diff --git a/screenshots/FileChooser_screenshot_2.png b/screenshots/FileChooser_screenshot_2.png new file mode 100644 index 0000000..470201d Binary files /dev/null and b/screenshots/FileChooser_screenshot_2.png differ diff --git a/screenshots/FileChooser_screenshot_3.png b/screenshots/FileChooser_screenshot_3.png new file mode 100644 index 0000000..b608f77 Binary files /dev/null and b/screenshots/FileChooser_screenshot_3.png differ diff --git a/screenshots/FileChooser_screenshot_4.png b/screenshots/FileChooser_screenshot_4.png new file mode 100644 index 0000000..b1bc5b0 Binary files /dev/null and b/screenshots/FileChooser_screenshot_4.png differ diff --git a/screenshots/FileChooser_screenshot_5.png b/screenshots/FileChooser_screenshot_5.png new file mode 100644 index 0000000..73da3dc Binary files /dev/null and b/screenshots/FileChooser_screenshot_5.png differ diff --git a/screenshots/FileChooser_screenshot_6.png b/screenshots/FileChooser_screenshot_6.png new file mode 100644 index 0000000..72053ad Binary files /dev/null and b/screenshots/FileChooser_screenshot_6.png differ diff --git a/screenshots/FileChooser_screenshot_7.png b/screenshots/FileChooser_screenshot_7.png new file mode 100644 index 0000000..729562e Binary files /dev/null and b/screenshots/FileChooser_screenshot_7.png differ diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..07c6cc2 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +from setuptools import setup, find_packages + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name="ipyfilechooser", + version="0.1b1", + author="Thomas Bouve (@crahan)", + author_email="crahan@n00.be", + description="""Python file chooser widget for use in + Jupyter/IPython in conjunction with ipywidgets""", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/crahan/ipyfilechooser", + packages=find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + ], + install_requires=[ + 'ipywidgets' + ] +)