{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Example: Building a File Browser with ipytree\n", "\n", "In this example, we demonstrate how to create an alternative file browser UI using the `ipytree` widget.\n", "\n", "## Collecting the files\n", "\n", "First let's define some imports." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install -q ipylab" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "from fnmatch import fnmatch\n", "from pathlib import PurePath" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since the example is living in the source repository, we also define a list of folders to exclude, so they are not displayed in the tree." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "EXCLUDES = {\n", " \".git\",\n", " \".github\",\n", " \".vscode\",\n", " \"build\",\n", " \"dist\",\n", " \"lib\",\n", " \"node_modules\",\n", " \"__pycache__\",\n", " \".ipynb_checkpoints\"\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's define a function to collect all the files, starting from a `root_path`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def collect_files(root_path='..'):\n", " files = []\n", " for dirpath, dirnames, filenames in os.walk(root_path, followlinks=True):\n", " dirnames[:] = [d for d in dirnames if d not in EXCLUDES]\n", " for f in filenames:\n", " fullpath = PurePath(dirpath).relative_to(root_path).joinpath(f)\n", "\n", " if fullpath.parts not in files:\n", " files.append(fullpath.parts)\n", " files.sort()\n", " return files" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "files = collect_files()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's show a subset of these files." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "files[:15]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's build a tree structure that will be used to build the tree widget." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tree = {}\n", "for f in files:\n", " node = tree\n", " for part in f:\n", " if part not in node:\n", " node[part] = {}\n", " node = node[part]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Building the tree widget\n", "\n", "We first import `ipytree`'s `Node` and `Tree` widgets." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipytree import Node, Tree\n", "from traitlets import Unicode" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following class derives from the base `Node` widget, and adds a `fullpath` property to store the full path to the file. This will be useful when opening the file using JupyterLab's command." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class TreeNode(Node):\n", " fullpath = Unicode(\"\").tag(sync=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following function traverse the tree structure created above, and creates the corresponding widgets." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def create_tree_widget(root, path, depth=0):\n", " node = Tree() if depth == 0 else TreeNode()\n", " for name, children in root.items():\n", " fullpath = path + [name]\n", " if len(children) == 0:\n", " leaf = TreeNode(name)\n", " leaf.fullpath = os.path.join(*fullpath)\n", " leaf.icon = 'file'\n", " leaf.icon_style = 'warning'\n", " node.add_node(leaf)\n", " else:\n", " subtree = create_tree_widget(children, fullpath, depth + 1)\n", " subtree.icon = 'folder'\n", " subtree.icon_style = 'info'\n", " subtree.name = name\n", " node.add_node(subtree)\n", " return node" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "file_tree = create_tree_widget(tree, [])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now display the tree in the notebook to make sure that it looks correct." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "file_tree" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Adding the tree to the left area in JupyterLab\n", "\n", "Now that the tree is ready, we can start adding it to other areas in JupyterLab outside of the notebook.\n", "\n", "Let's first create the frontend widget to serve as the proxy to JupyterLab." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipylab import JupyterFrontEnd" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "app = JupyterFrontEnd()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's also define a couple of buttons to:\n", "\n", "- open the selected files\n", "- expand all nodes of the tre\n", "- collapse all nodes of the tree" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Button, Layout, HBox, VBox\n", "\n", "open_button = Button(description='Open', button_style='success', icon='folder')\n", "expand_button = Button(description='Expand', button_style='info', icon='chevron-down')\n", "collapse_button = Button(description='Collapse', button_style='info', icon='chevron-right')\n", "hbox = HBox([\n", " open_button,\n", " expand_button,\n", " collapse_button\n", "], layout=Layout(overflow='unset'))\n", "hbox" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's now add the callbacks to catch click events." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def expand_tree(tree, expand=True):\n", " for node in tree.nodes:\n", " node.opened = expand\n", "\n", "def on_expand_click(b):\n", " expand_tree(file_tree)\n", "\n", " \n", "def on_collapse_click(b):\n", " expand_tree(file_tree, False)\n", "\n", "expand_button.on_click(on_expand_click)\n", "collapse_button.on_click(on_collapse_click)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When the \"Open\" button is clicked, we call `app.commands.execute` with the path to the file to open it in the JupyterLab interface." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def on_open_clicked(b):\n", " for node in file_tree.selected_nodes:\n", " filepath = node.fullpath\n", " if filepath:\n", " app.commands.execute('docmanager:open', { 'path': filepath})\n", "\n", "open_button.on_click(on_open_clicked)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's collapse the tree as its initial state, and add some overflow so it can be scrolled." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "expand_tree(file_tree, False)\n", "file_tree.layout = Layout(overflow='auto')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Panel will store both the buttons and the file tree right below." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipylab import Panel\n", "\n", "panel = Panel(children=[hbox, file_tree])\n", "panel.title.label = 'File Browser'\n", "panel.title.icon_class = 'jp-FileIcon'\n", "panel.layout = Layout(overflow='auto')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we can add the file browser to the left area! We can also change `'left'` to `'right'` if you prefer adding it to the right area." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "app.shell.add(panel, 'left', {'rank': 10000})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.2" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }