mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 18:38:11 +02:00
Add live Markdown editing, colours and files in the tree
This commit is contained in:
1 parent
0eecfba224
commit
f851ecd820
15 files changed
+1025
-58
No files matched your search
+77
-32
@@ -1,35 +1,68 @@
|
||||
"""The sidebar's directory tree, built like SaraFem's Filetree: one node per
|
||||
root, children loaded lazily on expand (every expand on a server is a
|
||||
request). Clicking a directory opens it in the browser."""
|
||||
"""The sidebar's tree, built like SaraFem's Filetree: one node per root,
|
||||
children loaded lazily on expand (every expand on a server is a request).
|
||||
Clicking a directory opens it in the browser; clicking a file opens it in
|
||||
the viewer. Files can be hidden from the tree in Settings > General."""
|
||||
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
import flet as ft
|
||||
|
||||
from App import App
|
||||
from Core import Palette
|
||||
from Core import FileIcons, Palette
|
||||
from Sources import JoinPath, SourceError
|
||||
|
||||
Location = tuple[str, str] # (Root, Path)
|
||||
|
||||
|
||||
def _TreeLabel(Name: str, OnTap: Callable[[], None]) -> tuple[ft.Text, ft.Container]:
|
||||
Label = ft.Text(Name, size=13, color=Palette.Text, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
|
||||
Holder = ft.Container(content=ft.GestureDetector(content=Label, on_tap=lambda e: OnTap()), padding=4, border_radius=4)
|
||||
return Label, Holder
|
||||
|
||||
|
||||
def _SetHighlight(Label: ft.Text, Holder: ft.Container, IsSelected: bool) -> None:
|
||||
Holder.bgcolor = Palette.Surface1 if IsSelected else None
|
||||
Label.color = Palette.Mauve if IsSelected else Palette.Text
|
||||
Holder.update()
|
||||
|
||||
|
||||
class _FileLeaf:
|
||||
"""A file in the tree: a plain row (no expand arrow) indented like a
|
||||
child tile, so names line up with the directories beside it."""
|
||||
|
||||
def __init__(self, Root: str, RelativePath: str, FileType: Optional[str],
|
||||
OnOpenFile: Callable[[str, str], None], Depth: int):
|
||||
Name = RelativePath.rsplit("/", 1)[-1]
|
||||
self.NameLabel, self.NameContainer = _TreeLabel(Name, lambda: OnOpenFile(Root, RelativePath))
|
||||
self.Tile = ft.Container(
|
||||
content=ft.Row(
|
||||
[ft.Icon(FileIcons.get(FileType or "", ft.Icons.INSERT_DRIVE_FILE_OUTLINED), size=16, color=Palette.Overlay0),
|
||||
ft.Container(self.NameContainer, expand=True)],
|
||||
spacing=12,
|
||||
),
|
||||
# Icon and name line up with a directory tile's at the same depth.
|
||||
padding=ft.Padding.only(left=8 + Depth * 14, right=8, top=2, bottom=2),
|
||||
)
|
||||
|
||||
def SetSelected(self, IsSelected: bool) -> None:
|
||||
_SetHighlight(self.NameLabel, self.NameContainer, IsSelected)
|
||||
|
||||
|
||||
class _FiletreeNode:
|
||||
def __init__(self, TheApp: App, Root: str, RelativePath: str, OnNavigate: Callable[[str, str], None], Depth: int):
|
||||
def __init__(self, TheApp: App, Root: str, RelativePath: str, OnNavigate: Callable[[str, str], None],
|
||||
OnOpenFile: Callable[[str, str], None], Depth: int):
|
||||
self.TheApp = TheApp
|
||||
self.Root = Root
|
||||
self.RelativePath = RelativePath
|
||||
self.OnNavigate = OnNavigate
|
||||
self.OnOpenFile = OnOpenFile
|
||||
self.Depth = Depth
|
||||
self.ChildrenLoaded = False
|
||||
self.ChildNodes: dict[str, "_FiletreeNode"] = {}
|
||||
self.FileLeaves: dict[str, _FileLeaf] = {}
|
||||
|
||||
Name = RelativePath.rsplit("/", 1)[-1] if RelativePath else Root
|
||||
self.NameLabel = ft.Text(Name, size=13, color=Palette.Text, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
|
||||
self.NameContainer = ft.Container(
|
||||
content=ft.GestureDetector(content=self.NameLabel, on_tap=lambda e: self.OnNavigate(self.Root, self.RelativePath)),
|
||||
padding=4,
|
||||
border_radius=4,
|
||||
)
|
||||
self.NameLabel, self.NameContainer = _TreeLabel(Name, lambda: self.OnNavigate(self.Root, self.RelativePath))
|
||||
self.Tile = ft.ExpansionTile(
|
||||
title=self.NameContainer,
|
||||
leading=ft.Icon(ft.Icons.STORAGE_OUTLINED if not RelativePath else ft.Icons.FOLDER_OUTLINED,
|
||||
@@ -50,30 +83,35 @@ class _FiletreeNode:
|
||||
|
||||
def LoadChildren(self, Update: bool = True) -> None:
|
||||
try:
|
||||
Directories = [Item.Name for Item in self.TheApp.Source.List(self.Root, self.RelativePath) if Item.IsDirectory]
|
||||
Items = self.TheApp.Source.List(self.Root, self.RelativePath)
|
||||
except SourceError:
|
||||
Directories = [] # locked, gone or unreachable: just no children
|
||||
Items = [] # locked, gone or unreachable: just no children
|
||||
self.ChildNodes = {}
|
||||
for Name in Directories:
|
||||
ChildPath = JoinPath(self.RelativePath, Name)
|
||||
self.ChildNodes[Name] = _FiletreeNode(self.TheApp, self.Root, ChildPath, self.OnNavigate, self.Depth + 1)
|
||||
self.Tile.controls = [Node.Tile for Node in self.ChildNodes.values()]
|
||||
self.FileLeaves = {}
|
||||
for Item in Items:
|
||||
ChildPath = JoinPath(self.RelativePath, Item.Name)
|
||||
if Item.IsDirectory:
|
||||
self.ChildNodes[Item.Name] = _FiletreeNode(self.TheApp, self.Root, ChildPath, self.OnNavigate,
|
||||
self.OnOpenFile, self.Depth + 1)
|
||||
elif self.TheApp.Config.ShowFilesInTree:
|
||||
self.FileLeaves[Item.Name] = _FileLeaf(self.Root, ChildPath, Item.FileType, self.OnOpenFile, self.Depth + 1)
|
||||
# Directories first, then files, like the file list.
|
||||
self.Tile.controls = [Node.Tile for Node in self.ChildNodes.values()] + [Leaf.Tile for Leaf in self.FileLeaves.values()]
|
||||
self.ChildrenLoaded = True
|
||||
if Update:
|
||||
self.Tile.update()
|
||||
|
||||
def SetSelected(self, IsSelected: bool) -> None:
|
||||
self.NameContainer.bgcolor = Palette.Surface1 if IsSelected else None
|
||||
self.NameLabel.color = Palette.Mauve if IsSelected else Palette.Text
|
||||
self.NameContainer.update()
|
||||
_SetHighlight(self.NameLabel, self.NameContainer, IsSelected)
|
||||
|
||||
|
||||
class Filetree(ft.Container):
|
||||
def __init__(self, TheApp: App, OnNavigate: Callable[[str, str], None]):
|
||||
def __init__(self, TheApp: App, OnNavigate: Callable[[str, str], None], OnOpenFile: Callable[[str, str], None]):
|
||||
self.TheApp = TheApp
|
||||
self.OnNavigate = OnNavigate
|
||||
self.OnOpenFile = OnOpenFile
|
||||
self.RootNodes: dict[str, _FiletreeNode] = {}
|
||||
self.SelectedNode: Optional[_FiletreeNode] = None
|
||||
self.SelectedNode: Optional[Union[_FiletreeNode, _FileLeaf]] = None
|
||||
self.TreeList = ft.ListView(expand=True)
|
||||
self.SourceLabel = ft.Text("", size=12, color=Palette.Overlay0, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
|
||||
super().__init__(
|
||||
@@ -87,7 +125,7 @@ class Filetree(ft.Container):
|
||||
|
||||
def Reload(self) -> None:
|
||||
"""Rebuilds the tree from the current source (after connecting,
|
||||
logging in or out, or a change that moved directories around)."""
|
||||
logging in or out, or a change that moved things around)."""
|
||||
Source = self.TheApp.Source
|
||||
self.SourceLabel.value = "Not connected" if self.TheApp.ConnectError else Source.Describe()
|
||||
self.SelectedNode = None
|
||||
@@ -98,7 +136,7 @@ class Filetree(ft.Container):
|
||||
except SourceError:
|
||||
Roots = []
|
||||
Expanded = {Root: Node.Tile.expanded for Root, Node in self.RootNodes.items()}
|
||||
self.RootNodes = {Root: _FiletreeNode(self.TheApp, Root, "", self.OnNavigate, 0) for Root in Roots}
|
||||
self.RootNodes = {Root: _FiletreeNode(self.TheApp, Root, "", self.OnNavigate, self.OnOpenFile, 0) for Root in Roots}
|
||||
for Root, Node in self.RootNodes.items():
|
||||
# One root (always the case locally) starts open; otherwise keep what was open.
|
||||
if Expanded.get(Root, len(Roots) == 1):
|
||||
@@ -108,21 +146,28 @@ class Filetree(ft.Container):
|
||||
self.update()
|
||||
|
||||
def RevealPath(self, Root: Optional[str], RelativePath: str) -> None:
|
||||
"""Expand and highlight the tree down to a directory, loading as needed."""
|
||||
"""Expand and highlight the tree down to a directory or file, loading
|
||||
as needed."""
|
||||
if self.SelectedNode:
|
||||
self.SelectedNode.SetSelected(False)
|
||||
self.SelectedNode = None
|
||||
Node = self.RootNodes.get(Root or "")
|
||||
if Node is None:
|
||||
return
|
||||
for Part in [Part for Part in RelativePath.split("/") if Part]:
|
||||
Target: Union[_FiletreeNode, _FileLeaf] = Node
|
||||
Parts = [Part for Part in RelativePath.split("/") if Part]
|
||||
for Index, Part in enumerate(Parts):
|
||||
if not Node.ChildrenLoaded:
|
||||
Node.LoadChildren()
|
||||
NextNode = Node.ChildNodes.get(Part)
|
||||
if NextNode is None:
|
||||
break # hidden or not a directory: highlight the closest parent
|
||||
Leaf = Node.FileLeaves.get(Part) if Index == len(Parts) - 1 else None
|
||||
if NextNode is None and Leaf is None:
|
||||
break # hidden: highlight the closest parent
|
||||
Node.Tile.expanded = True
|
||||
Node.Tile.update()
|
||||
Node = NextNode
|
||||
Node.SetSelected(True)
|
||||
self.SelectedNode = Node
|
||||
if Leaf is not None:
|
||||
Target = Leaf
|
||||
break
|
||||
Node = Target = NextNode
|
||||
Target.SetSelected(True)
|
||||
self.SelectedNode = Target
|
||||
Reference in new issue
Block a user