Files
SaraFem/Picker.py
T
2026-09-24 22:03:58 +02:00

196 lines
7.5 KiB
Python

"""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)