mirror of
https://git.srhtnin.garden/sarah/SaraFem.git
synced 2026-10-13 18:18:11 +02:00
160 lines
5.7 KiB
Python
160 lines
5.7 KiB
Python
"""Real hierarchical directory tree for the sidebar, separate from Bookmarks
|
|
(see the mockup: sidebar splits into a bookmarks section and a filetree section).
|
|
Children are loaded lazily on expand, since the tree can be rooted at "/".
|
|
Each node also accepts drops from the Browser's file list, moving the dropped
|
|
entry into that directory."""
|
|
|
|
from pathlib import Path
|
|
from typing import Callable, Optional
|
|
|
|
import flet as ft
|
|
|
|
from Core import AppConfig, DefaultFiletreeRoot, DragGroup, MoveIntoDirectory, NotificationStyle, Palette
|
|
|
|
|
|
class _FiletreeNode:
|
|
def __init__(
|
|
self,
|
|
EntryPath: Path,
|
|
Config: AppConfig,
|
|
OnNavigate: Callable[[Path], None],
|
|
Depth: int,
|
|
OnFilesMoved: Optional[Callable[[], None]] = None,
|
|
):
|
|
self.EntryPath = EntryPath
|
|
self.Config = Config
|
|
self.OnNavigate = OnNavigate
|
|
self.Depth = Depth
|
|
self.OnFilesMoved = OnFilesMoved
|
|
self.ChildrenLoaded = False
|
|
self.ChildNodes: dict[Path, "_FiletreeNode"] = {}
|
|
self.IsSelected = False
|
|
|
|
self.NameLabel = ft.Text(EntryPath.name or str(EntryPath), size=13, color=Palette.Text)
|
|
self.NameContainer = ft.Container(
|
|
content=ft.GestureDetector(
|
|
content=self.NameLabel,
|
|
on_tap=lambda e: self.OnNavigate(self.EntryPath),
|
|
),
|
|
padding=4,
|
|
border_radius=4,
|
|
)
|
|
|
|
self.Tile = ft.ExpansionTile(
|
|
title=ft.DragTarget(group=DragGroup, content=self.NameContainer, on_accept=self._OnEntryDropped),
|
|
leading=ft.Icon(ft.Icons.FOLDER_OUTLINED, size=16, color=Palette.Overlay0),
|
|
controls=[],
|
|
tile_padding=ft.Padding.only(left=8 + Depth * 14, right=8),
|
|
icon_color=Palette.Overlay0,
|
|
collapsed_icon_color=Palette.Overlay0,
|
|
on_change=self._OnExpansionChanged,
|
|
)
|
|
|
|
def _OnExpansionChanged(self, e) -> None:
|
|
if self.Tile.expanded and not self.ChildrenLoaded:
|
|
self._LoadChildren()
|
|
|
|
def _LoadChildren(self) -> None:
|
|
try:
|
|
SubDirs = sorted(
|
|
(p for p in self.EntryPath.iterdir() if p.is_dir()),
|
|
key=lambda p: p.name.lower(),
|
|
)
|
|
except (PermissionError, FileNotFoundError, OSError):
|
|
SubDirs = []
|
|
|
|
if not self.Config.ShowDotfiles:
|
|
SubDirs = [p for p in SubDirs if not p.name.startswith(".")]
|
|
|
|
self.ChildNodes = {}
|
|
Controls = []
|
|
for SubDir in SubDirs:
|
|
Node = _FiletreeNode(SubDir, self.Config, self.OnNavigate, self.Depth + 1, self.OnFilesMoved)
|
|
self.ChildNodes[SubDir] = Node
|
|
Controls.append(Node.Tile)
|
|
self.Tile.controls = Controls
|
|
self.ChildrenLoaded = True
|
|
self.Tile.update()
|
|
|
|
def _OnEntryDropped(self, e: ft.DragTargetEvent) -> None:
|
|
SourcePath = Path(e.src.data)
|
|
try:
|
|
MoveIntoDirectory(SourcePath, self.EntryPath)
|
|
except OSError as Error:
|
|
self._Notify(str(Error))
|
|
return
|
|
|
|
if self.ChildrenLoaded:
|
|
self._LoadChildren()
|
|
if self.OnFilesMoved:
|
|
self.OnFilesMoved()
|
|
self._Notify(f'Moved "{SourcePath.name}" to {self.EntryPath.name or self.EntryPath}.', Kind="success")
|
|
|
|
def _Notify(self, Message: str, Kind: str = "error") -> None:
|
|
Page = self.NameContainer.page
|
|
Background, Accent = NotificationStyle(Kind)
|
|
Page.show_dialog(
|
|
ft.SnackBar(
|
|
content=ft.Text(Message, color=Accent),
|
|
bgcolor=Background,
|
|
shape=ft.RoundedRectangleBorder(radius=4, side=ft.BorderSide(1, Accent)),
|
|
open=True,
|
|
)
|
|
)
|
|
|
|
def SetSelected(self, IsSelected: bool) -> None:
|
|
self.IsSelected = IsSelected
|
|
self.NameContainer.bgcolor = Palette.Surface1 if IsSelected else None
|
|
self.NameLabel.color = Palette.Mauve if IsSelected else Palette.Text
|
|
self.NameContainer.update()
|
|
|
|
|
|
class Filetree(ft.Container):
|
|
def __init__(
|
|
self,
|
|
OnNavigate: Callable[[Path], None],
|
|
Config: AppConfig,
|
|
RootPath: Optional[Path] = None,
|
|
OnFilesMoved: Optional[Callable[[], None]] = None,
|
|
):
|
|
self.OnNavigate = OnNavigate
|
|
self.Config = Config
|
|
self.RootPath = RootPath or Path(DefaultFiletreeRoot)
|
|
self.OnFilesMoved = OnFilesMoved
|
|
self.SelectedNode: Optional[_FiletreeNode] = None
|
|
|
|
self.RootNode = _FiletreeNode(self.RootPath, Config, OnNavigate, Depth=0, OnFilesMoved=OnFilesMoved)
|
|
|
|
super().__init__(
|
|
content=ft.ListView(controls=[self.RootNode.Tile], expand=True),
|
|
expand=True,
|
|
bgcolor=Palette.Mantle,
|
|
)
|
|
|
|
def RevealPath(self, TargetPath: Path) -> None:
|
|
"""Expand + highlight the tree down to TargetPath, lazily loading as needed."""
|
|
if self.SelectedNode:
|
|
self.SelectedNode.SetSelected(False)
|
|
self.SelectedNode = None
|
|
|
|
try:
|
|
Relative = TargetPath.relative_to(self.RootPath)
|
|
except ValueError:
|
|
return # TargetPath isn't under this tree's root
|
|
|
|
Node = self.RootNode
|
|
Parts = Relative.parts if str(Relative) != "." else ()
|
|
for Part in Parts:
|
|
if not Node.ChildrenLoaded:
|
|
Node._LoadChildren()
|
|
NextPath = Node.EntryPath / Part
|
|
NextNode = Node.ChildNodes.get(NextPath)
|
|
if NextNode is None:
|
|
return # hidden, permission denied, or not a directory
|
|
Node.Tile.expanded = True
|
|
Node.Tile.update()
|
|
Node = NextNode
|
|
|
|
Node.SetSelected(True)
|
|
self.SelectedNode = Node
|