Source code for openairclim.gui.components.file_picker

"""File/folder picker component: text input + native browse dialog."""

from pathlib import Path

import panel as pn
import param


def _shorten_path(value, keep=2):
    """Return the last `keep` path segments, prefixed with "..." if longer.

    Args:
        value (str): Path string.
        keep (int): Number of trailing path segments to keep.

    Returns:
        str: Shortened path, e.g. ".../oac/example" — or the original
            string unchanged if it already has `keep` segments or fewer.
    """
    parts = Path(value).parts
    if len(parts) <= keep:
        return value
    return ".../" + "/".join(parts[-keep:])


[docs] class FilePicker(pn.viewable.Viewer): """A picker with a text field, browse button, and status indicator. Args: label (str): Display label shown above the text input. file_types (list): File type filters for the browse dialog. directory (bool): If True, browse for a folder instead of a file. path (str): Currently selected path (readable and watchable). placeholder (str, optional): Placeholder text shown in the text input when empty. Defaults to a generic "enter a path..." hint; pass this to say something more specific instead (e.g. what an empty value means for that particular field). """ label = param.String(default="File") file_types = param.List( default=[("All files", "*.*")], doc="File type filters as (description, pattern) tuples.", ) directory = param.Boolean( default=False, doc="If True, select a directory instead of a file.", ) path = param.String(default="", doc="Currently selected path.") description = param.String( default=None, allow_None=True, doc="Tooltip text (Markdown supported) shown as an (i) icon next " "to the label — same convention as Panel widgets' own " "`description`, passed straight through to the underlying " "TextInput.", ) placeholder = param.String( default=None, allow_None=True, doc="Placeholder text for the text input. Defaults to a generic " "'enter a path...' hint if not given.", ) def __init__(self, **params): super().__init__(**params) default_placeholder = ( "Enter a folder path or click the folder icon..." if self.directory else "Enter a file path or click the folder icon..." ) self._text_input = pn.widgets.TextInput( name=self.label, value=self.path, placeholder=self.placeholder or default_placeholder, description=self.description, ) self._browse_btn = pn.widgets.Button( icon="folder", icon_size="1.1em", button_type="default", width=44, margin=(24, 10, 0, 6), ) self._status = pn.pane.Markdown( "", styles={"font-size": "0.9em"}, margin=(0, 5, 0, 5), ) self._browse_btn.on_click(self._on_browse) self._text_input.param.watch(self._on_text_changed, "value")
[docs] def set_path(self, value: str) -> None: """Programmatically set the selected path. Updates the text input, which in turn syncs `self.path` and the status indicator via the existing `_on_text_changed` watcher.""" self._text_input.value = value
# ------------------------------------------------------------------ # Event handlers # ------------------------------------------------------------------ def _on_browse(self, _event): """Open a native file/folder dialog and update the path.""" import tkinter as tk from tkinter import filedialog root = tk.Tk() root.withdraw() root.attributes("-topmost", True) if self.directory: selected = filedialog.askdirectory( title=f"Select {self.label}", ) else: selected = filedialog.askopenfilename( title=f"Select {self.label}", filetypes=self.file_types, ) root.destroy() if selected: self._text_input.value = selected def _on_text_changed(self, event): """Sync the text input value to the public *path* param.""" self.path = event.new self._update_status(event.new) # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ def _update_status(self, value): """Show a short status hint below the input. The path itself is shortened to its last two segments (e.g. ".../oac/example") so the status stays on one line even for long, deeply-nested paths. """ if not value: self._status.object = "" return target = "Folder" if self.directory else "File" short = _shorten_path(value) exists = Path(value).is_dir() if self.directory else Path(value).is_file() if exists: self._status.object = f'✅ {target} "{short}" exists.' else: self._status.object = f'⚠️ {target} not found: "{short}"' # ------------------------------------------------------------------ # Panel rendering # ------------------------------------------------------------------ def __panel__(self): return pn.Column( pn.Row(self._text_input, self._browse_btn), self._status, sizing_mode="stretch_width", )