mirror of
https://git.srhtnin.garden/sarah/SaraFem.git
synced 2026-10-15 10:23:17 +02:00
Initial commit
This commit is contained in:
commit
4c8d272719
31 files changed
+3870
No files matched your search
@@ -0,0 +1,195 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user