"""File-picker mode. The picker is the normal SaraFem window (sidebar, toolbar, list/grid, preview, the same menus and shortcuts) with this bar along the bottom: a file name field when saving, a file type filter when the app asked for one, and Cancel/confirm. Used two ways: - `python Main.py --pick-open/--pick-save/--pick-folder ...` directly, for scripts. - Launched as a subprocess by PortalService.py, which is what actually makes SaraFem show up as the system file picker (the org.freedesktop.impl.portal.FileChooser backend); see that file for the D-Bus side of this. Either way, the result goes to the OnComplete callback (Main.py writes it to --result-file as JSON); nothing here talks D-Bus directly.""" from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Callable, Optional import flet as ft from Core import ChipButtonStyle, Palette if TYPE_CHECKING: from Browser import Browser @dataclass class PickRequest: Kind: str # "open" or "save" Multiple: bool = False Directory: bool = False Title: str = "Select a File" SuggestedName: str = "" StartPath: Path = field(default_factory=Path.home) # (filter label, [glob patterns]), offered in a dropdown with the first # one selected, like other pickers. Empty list means "show everything". Filters: list[tuple[str, list[str]]] = field(default_factory=list) class PickerBar(ft.Container): def __init__(self, Request: PickRequest, OnComplete: Callable[[Optional[list[Path]]], None]): self.Request = Request self.OnComplete = OnComplete self.BrowserView: Optional["Browser"] = None self.FilterIndex = 0 if Request.Filters else -1 self.Completed = False Controls: list[ft.Control] = [] self.NameField: Optional[ft.TextField] = None if Request.Kind == "save": self.NameField = ft.TextField( value=Request.SuggestedName, label="File name", expand=True, autofocus=True, on_submit=lambda e: self.Confirm(), on_focus=lambda e: self._PauseShortcuts(True), on_blur=lambda e: self._PauseShortcuts(False), ) Controls.append(self.NameField) else: Controls.append(ft.Container(expand=True)) if Request.Filters and not Request.Directory: Controls.append(ft.Dropdown( width=260, value="0", options=[ ft.DropdownOption(key=str(Index), text=f"{Label} ({', '.join(Patterns)})") for Index, (Label, Patterns) in enumerate(Request.Filters) ] + [ft.DropdownOption(key="-1", text="All files")], on_select=self._OnFilterSelected, )) Controls.append(ft.OutlinedButton(content="Cancel", style=ChipButtonStyle(), on_click=lambda e: self.Cancel())) Controls.append(ft.OutlinedButton(content=self._ConfirmLabel(), style=ChipButtonStyle(), on_click=lambda e: self.Confirm())) super().__init__( content=ft.Row(Controls, spacing=8, vertical_alignment=ft.CrossAxisAlignment.CENTER), padding=8, bgcolor=Palette.Mantle, border=ft.Border.only(top=ft.BorderSide(1, Palette.Surface1)), ) def Attach(self, BrowserView: "Browser") -> None: self.BrowserView = BrowserView self._PauseShortcuts(self.NameField is not None) # the name field starts focused def _PauseShortcuts(self, Paused: bool) -> None: if self.BrowserView is not None: self.BrowserView.ShortcutsPaused = Paused def _ConfirmLabel(self) -> str: if self.Request.Kind == "save": return "Save" if self.Request.Directory: return "Select Folder" return "Open" # --- Hooks the Browser calls ------------------------------------------ def Filter(self, EntryPath: Path) -> bool: if EntryPath.is_dir(): return True if self.Request.Directory: return False if self.FilterIndex < 0: return True _Label, Patterns = self.Request.Filters[self.FilterIndex] NameLower = EntryPath.name.lower() return any(Path(NameLower).match(Pattern.lower()) for Pattern in Patterns) def Activate(self, EntryPath: Path) -> None: """Double-click/Enter on a file.""" if self.Request.Kind == "save" and self.NameField is not None: self.NameField.value = EntryPath.name self.Confirm() elif not self.Request.Directory: self._Finish([EntryPath]) def SelectionChanged(self) -> None: # Picking an existing file while saving fills in its name, like other pickers. if self.Request.Kind != "save" or self.NameField is None or self.BrowserView is None: return Selected = self.BrowserView.SelectedPath if Selected is not None and not Selected.is_dir() and self.NameField.value != Selected.name: self.NameField.value = Selected.name self.NameField.update() def _OnFilterSelected(self, Event) -> None: self.FilterIndex = int(Event.control.value) if self.BrowserView is not None: self.BrowserView.Refresh() # --- Confirm / cancel --------------------------------------------------- def Confirm(self) -> None: View = self.BrowserView if View is None: return Selected = [Entry for Entry in View.CurrentEntries if Entry in View.SelectedPaths] if self.Request.Kind == "save": Name = (self.NameField.value or "").strip() if not Name: return Target = View.CurrentPath / Name if Target.is_dir(): View.NavigateTo(Target) elif Target.exists(): self._ConfirmOverwrite(Target) else: self._Finish([Target]) return if self.Request.Directory: Folders = [Entry for Entry in Selected if Entry.is_dir()] if not self.Request.Multiple: Folders = Folders[:1] self._Finish(Folders or [View.CurrentPath]) return if not self.Request.Multiple: Selected = [View.SelectedPath] if View.SelectedPath is not None else [] if len(Selected) == 1 and Selected[0].is_dir(): View.NavigateTo(Selected[0]) # "Open" on a folder goes into it return Files = [Entry for Entry in Selected if not Entry.is_dir()] if Files: self._Finish(Files) def _ConfirmOverwrite(self, Target: Path) -> None: def Replace(e) -> None: self.page.pop_dialog() self._Finish([Target]) self.page.show_dialog(ft.AlertDialog( title="Replace file?", content=ft.Text(f'"{Target.name}" already exists in {Target.parent}. Replace it?'), actions=[ ft.OutlinedButton(content="Cancel", style=ChipButtonStyle(), on_click=lambda e: self.page.pop_dialog()), ft.OutlinedButton(content="Replace", style=ChipButtonStyle(), on_click=Replace), ], )) def _Finish(self, Paths: list[Path]) -> None: # Enter in the name field fires both its on_submit and the page's # keyboard handler, so this can be reached twice for one confirm. if self.Completed: return self.Completed = True self.OnComplete(Paths) def Cancel(self) -> None: if not self.Completed: self.Completed = True self.OnComplete(None)