mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-15 10:43:17 +02:00
Properties, and ticking checkboxes without editing (client)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c8d29ad6f8
commit
ca37409e37
4 files changed
+238
-22
No files matched your search
+7
-5
@@ -146,10 +146,12 @@ def MaybeDiagram(TheApp, BlockText: str, FontSize: int) -> Optional[ft.Control]:
|
|||||||
return DiagramBlock(TheApp, Source, FontSize)
|
return DiagramBlock(TheApp, Source, FontSize)
|
||||||
|
|
||||||
|
|
||||||
def WithDiagrams(TheApp, Document: str, FontSize: int, MakeMarkdown: Callable[[str], ft.Control]) -> list[ft.Control]:
|
def WithDiagrams(TheApp, Document: str, FontSize: int,
|
||||||
"""The rendered view's controls: the Markdown, split around its diagrams."""
|
MakeMarkdown: Callable[[str, int], ft.Control]) -> list[ft.Control]:
|
||||||
|
"""The rendered view's controls: the Markdown, split around its diagrams.
|
||||||
|
MakeMarkdown(Part, Start) gets each piece and where it starts in Document."""
|
||||||
if not CanDraw(TheApp) or not any(DiagramFence.match(Line) for Line in Document.split("\n")):
|
if not CanDraw(TheApp) or not any(DiagramFence.match(Line) for Line in Document.split("\n")):
|
||||||
return [MakeMarkdown(Document)]
|
return [MakeMarkdown(Document, 0)]
|
||||||
from LiveMarkdown import SplitBlocks # LiveMarkdown imports this module
|
from LiveMarkdown import SplitBlocks # LiveMarkdown imports this module
|
||||||
|
|
||||||
Controls: list[ft.Control] = []
|
Controls: list[ft.Control] = []
|
||||||
@@ -159,9 +161,9 @@ def WithDiagrams(TheApp, Document: str, FontSize: int, MakeMarkdown: Callable[[s
|
|||||||
if Source is None:
|
if Source is None:
|
||||||
continue
|
continue
|
||||||
if Document[Position:Block.Start].strip():
|
if Document[Position:Block.Start].strip():
|
||||||
Controls.append(MakeMarkdown(Document[Position:Block.Start]))
|
Controls.append(MakeMarkdown(Document[Position:Block.Start], Position))
|
||||||
Controls.append(DiagramBlock(TheApp, Source, FontSize))
|
Controls.append(DiagramBlock(TheApp, Source, FontSize))
|
||||||
Position = Block.End
|
Position = Block.End
|
||||||
if Document[Position:].strip():
|
if Document[Position:].strip():
|
||||||
Controls.append(MakeMarkdown(Document[Position:]))
|
Controls.append(MakeMarkdown(Document[Position:], Position))
|
||||||
return Controls
|
return Controls
|
||||||
+45
-13
@@ -7,7 +7,9 @@ A block is what Markdown itself separates: a heading line, a fenced code
|
|||||||
block, a horizontal rule, or a run of lines up to a blank line (a paragraph,
|
block, a horizontal rule, or a run of lines up to a blank line (a paragraph,
|
||||||
list, table or quote). The text between blocks (blank lines) is kept
|
list, table or quote). The text between blocks (blank lines) is kept
|
||||||
byte for byte, so editing one block never reformats the rest of the file.
|
byte for byte, so editing one block never reformats the rest of the file.
|
||||||
A ```mermaid block shows as its diagram when MakeDiagram gives one (Diagrams.py)."""
|
A ```mermaid block shows as its diagram when MakeDiagram gives one (Diagrams.py),
|
||||||
|
and the properties block at the top as its panel (Properties.py). Checkboxes
|
||||||
|
(tasks and true/false properties) tick without opening their block."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -17,6 +19,7 @@ import flet as ft
|
|||||||
|
|
||||||
import Core
|
import Core
|
||||||
from Core import CodeStyleSheet, MarkdownCodeTheme, MarkdownSheet, Palette
|
from Core import CodeStyleSheet, MarkdownCodeTheme, MarkdownSheet, Palette
|
||||||
|
from Properties import ParseProperties, PropertiesView, ToggleLine, WithCheckboxes
|
||||||
|
|
||||||
FenceStart = re.compile(r"^ {0,3}(`{3,}|~{3,})")
|
FenceStart = re.compile(r"^ {0,3}(`{3,}|~{3,})")
|
||||||
HeadingLine = re.compile(r"^ {0,3}(#{1,6})(\s|$)")
|
HeadingLine = re.compile(r"^ {0,3}(#{1,6})(\s|$)")
|
||||||
@@ -28,7 +31,7 @@ IndentedLine = re.compile(r"^( {2,}|\t)\S")
|
|||||||
class Block:
|
class Block:
|
||||||
Start: int # offsets into the document; Text = Document[Start:End]
|
Start: int # offsets into the document; Text = Document[Start:End]
|
||||||
End: int
|
End: int
|
||||||
Kind: str # "Heading", "Code", "Rule" or "Text"
|
Kind: str # "Properties", "Heading", "Code", "Rule" or "Text"
|
||||||
StartLine: int
|
StartLine: int
|
||||||
Level: int = 0 # heading level
|
Level: int = 0 # heading level
|
||||||
|
|
||||||
@@ -49,6 +52,11 @@ def SplitBlocks(Document: str) -> list[Block]:
|
|||||||
LastLine = -1 # last non-blank line of the open Text block
|
LastLine = -1 # last non-blank line of the open Text block
|
||||||
Fence: Optional[tuple[str, int]] = None # (character, length) of an open code fence
|
Fence: Optional[tuple[str, int]] = None # (character, length) of an open code fence
|
||||||
FenceFrom = 0
|
FenceFrom = 0
|
||||||
|
Found = ParseProperties(Document)
|
||||||
|
SkipUntil = -1
|
||||||
|
if Found is not None:
|
||||||
|
Blocks.append(Block(0, LineEnd(Found.EndLine), "Properties", 0))
|
||||||
|
SkipUntil = Found.EndLine
|
||||||
|
|
||||||
def CloseCurrent() -> None:
|
def CloseCurrent() -> None:
|
||||||
nonlocal Current
|
nonlocal Current
|
||||||
@@ -57,6 +65,8 @@ def SplitBlocks(Document: str) -> list[Block]:
|
|||||||
Current = None
|
Current = None
|
||||||
|
|
||||||
for Index, Line in enumerate(Lines):
|
for Index, Line in enumerate(Lines):
|
||||||
|
if Index <= SkipUntil:
|
||||||
|
continue
|
||||||
if Fence is not None:
|
if Fence is not None:
|
||||||
Match = FenceStart.match(Line)
|
Match = FenceStart.match(Line)
|
||||||
if Match and Match.group(1)[0] == Fence[0] and len(Match.group(1)) >= Fence[1] and not Line.strip()[len(Match.group(1)):].strip():
|
if Match and Match.group(1)[0] == Fence[0] and len(Match.group(1)) >= Fence[1] and not Line.strip()[len(Match.group(1)):].strip():
|
||||||
@@ -116,7 +126,7 @@ class LiveMarkdown(ft.Column):
|
|||||||
self.BlockControls: list[ft.Control] = []
|
self.BlockControls: list[ft.Control] = []
|
||||||
# Rendered blocks by their text, reused when the layout is rebuilt so
|
# Rendered blocks by their text, reused when the layout is rebuilt so
|
||||||
# only the edited part is rendered again.
|
# only the edited part is rendered again.
|
||||||
self.RenderCache: dict[str, list[ft.Control]] = {}
|
self.RenderCache: dict[tuple[str, str], list[ft.Control]] = {}
|
||||||
self.EditingIndex: Optional[int] = None # len(Blocks) is the new block at the end
|
self.EditingIndex: Optional[int] = None # len(Blocks) is the new block at the end
|
||||||
self.Field: Optional[ft.TextField] = None
|
self.Field: Optional[ft.TextField] = None
|
||||||
self._Layout(Update=False)
|
self._Layout(Update=False)
|
||||||
@@ -219,24 +229,46 @@ class LiveMarkdown(ft.Column):
|
|||||||
|
|
||||||
# --- Rendering -------------------------------------------------------
|
# --- Rendering -------------------------------------------------------
|
||||||
|
|
||||||
def _Render(self, Text: str) -> ft.Control:
|
def _Markdown(self, Text: str) -> ft.Control:
|
||||||
Diagram = self.MakeDiagram(Text, self.FontSize) if self.MakeDiagram else None
|
return ft.Markdown(
|
||||||
Rendered = ft.Container(
|
|
||||||
content=Diagram or ft.Markdown(
|
|
||||||
value=Text,
|
value=Text,
|
||||||
extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED,
|
extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED,
|
||||||
md_style_sheet=MarkdownSheet(self.FontSize),
|
md_style_sheet=MarkdownSheet(self.FontSize),
|
||||||
code_theme=MarkdownCodeTheme(),
|
code_theme=MarkdownCodeTheme(),
|
||||||
code_style_sheet=CodeStyleSheet(),
|
code_style_sheet=CodeStyleSheet(),
|
||||||
on_tap_link=self.OnTapLink,
|
on_tap_link=self.OnTapLink,
|
||||||
),
|
|
||||||
padding=ft.Padding.symmetric(horizontal=8, vertical=6),
|
|
||||||
border_radius=4,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _Render(self, Text: str, Kind: str) -> ft.Control:
|
||||||
|
Rendered = ft.Container(padding=ft.Padding.symmetric(horizontal=8, vertical=6), border_radius=4)
|
||||||
|
# Lines are counted from the block's start, found again when ticked:
|
||||||
|
# a rendered block is reused wherever its text ends up (RenderCache).
|
||||||
|
Toggle = lambda Line, C=Rendered: self._OnToggle(C, Line)
|
||||||
|
Diagram = self.MakeDiagram(Text, self.FontSize) if self.MakeDiagram and Kind == "Code" else None
|
||||||
|
if Diagram is not None:
|
||||||
|
Rendered.content = Diagram
|
||||||
|
elif Kind == "Properties" and (Found := ParseProperties(Text)) is not None:
|
||||||
|
Rendered.content = PropertiesView(Found, self.FontSize, self._Markdown, Toggle)
|
||||||
|
elif Kind == "Text":
|
||||||
|
Parts = WithCheckboxes(Text, 0, self._Markdown, Toggle, self.FontSize)
|
||||||
|
Rendered.content = Parts[0] if len(Parts) == 1 else ft.Column(Parts, spacing=0, tight=True)
|
||||||
|
else:
|
||||||
|
Rendered.content = self._Markdown(Text)
|
||||||
Rendered.on_click = lambda e, C=Rendered: self._OnBlockClicked(C)
|
Rendered.on_click = lambda e, C=Rendered: self._OnBlockClicked(C)
|
||||||
Rendered.on_hover = lambda e, C=Rendered: self._OnHover(C, e)
|
Rendered.on_hover = lambda e, C=Rendered: self._OnHover(C, e)
|
||||||
return Rendered
|
return Rendered
|
||||||
|
|
||||||
|
def _OnToggle(self, Control: ft.Control, Line: int) -> None:
|
||||||
|
"""A checkbox in a rendered block: flip its line, leave the block shut."""
|
||||||
|
if self.EditingIndex is not None:
|
||||||
|
self.Commit()
|
||||||
|
for Index, Controls in enumerate(self.BlockControls):
|
||||||
|
if Controls is Control:
|
||||||
|
self.Document = ToggleLine(self.Document, self.Blocks[Index].StartLine + Line)
|
||||||
|
self.OnChange()
|
||||||
|
self._Layout()
|
||||||
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _OnHover(Control: ft.Container, Event) -> None:
|
def _OnHover(Control: ft.Container, Event) -> None:
|
||||||
Control.bgcolor = Palette.Mantle if str(Event.data).lower() == "true" else None
|
Control.bgcolor = Palette.Mantle if str(Event.data).lower() == "true" else None
|
||||||
@@ -252,11 +284,11 @@ class LiveMarkdown(ft.Column):
|
|||||||
self.BlockControls.append(self.Field)
|
self.BlockControls.append(self.Field)
|
||||||
continue
|
continue
|
||||||
Text = self.Document[Item.Start:Item.End]
|
Text = self.Document[Item.Start:Item.End]
|
||||||
Reused = Cache.get(Text)
|
Reused = Cache.get((Item.Kind, Text))
|
||||||
Control = Reused.pop() if Reused else self._Render(Text)
|
Control = Reused.pop() if Reused else self._Render(Text, Item.Kind)
|
||||||
Control.bgcolor = None
|
Control.bgcolor = None
|
||||||
self.BlockControls.append(Control)
|
self.BlockControls.append(Control)
|
||||||
self.RenderCache.setdefault(Text, []).append(Control)
|
self.RenderCache.setdefault((Item.Kind, Text), []).append(Control)
|
||||||
Controls = list(self.BlockControls)
|
Controls = list(self.BlockControls)
|
||||||
if self.EditingIndex == len(self.Blocks):
|
if self.EditingIndex == len(self.Blocks):
|
||||||
Controls.append(self.Field)
|
Controls.append(self.Field)
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""Properties, like Obsidian's (and the web view's Frontend/src/Properties.js):
|
||||||
|
a block at the very top of a note,
|
||||||
|
|
||||||
|
---
|
||||||
|
Key: Value
|
||||||
|
Done: true
|
||||||
|
---
|
||||||
|
|
||||||
|
shown as a Properties panel instead of text. true/false values are
|
||||||
|
checkboxes, and so are task list items (- [ ]): both can be ticked without
|
||||||
|
editing the note. The parsing follows Properties.js so a note reads the same
|
||||||
|
in both."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
import flet as ft
|
||||||
|
|
||||||
|
from Core import Palette
|
||||||
|
|
||||||
|
OpenOrClose = re.compile(r"^---\s*$")
|
||||||
|
PropertyLine = re.compile(r"^([^:\s][^:]*?)\s*:\s*(.*)$")
|
||||||
|
TaskLine = re.compile(r"^(\s*(?:[-*+]|\d+[.)])\s+)\[( |x|X)\]")
|
||||||
|
BooleanValue = re.compile(r"^(true|false)$", re.IGNORECASE)
|
||||||
|
|
||||||
|
# Ticks a box: called with the line (0-based, in the whole note) to flip.
|
||||||
|
OnToggle = Optional[Callable[[int], None]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Property:
|
||||||
|
Key: str
|
||||||
|
Value: str
|
||||||
|
Line: int
|
||||||
|
List: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Parsed:
|
||||||
|
Properties: list[Property]
|
||||||
|
EndLine: int # the closing ---
|
||||||
|
EndOffset: int # just after it
|
||||||
|
|
||||||
|
|
||||||
|
def ParseProperties(Text: str) -> Optional[Parsed]:
|
||||||
|
"""The block at the top, or None (also when it isn't all Key: Value
|
||||||
|
lines: then it's just a note that starts with a --- rule)."""
|
||||||
|
Lines = Text.split("\n")
|
||||||
|
if not OpenOrClose.match(Lines[0]):
|
||||||
|
return None
|
||||||
|
EndLine = next((Index for Index in range(1, len(Lines)) if OpenOrClose.match(Lines[Index])), None)
|
||||||
|
if EndLine is None:
|
||||||
|
return None
|
||||||
|
Found: list[Property] = []
|
||||||
|
for Index in range(1, EndLine):
|
||||||
|
Line = Lines[Index]
|
||||||
|
Match = PropertyLine.match(Line)
|
||||||
|
if Match:
|
||||||
|
Found.append(Property(Match.group(1), Match.group(2).strip(), Index))
|
||||||
|
elif Line.strip().startswith("- ") and Found:
|
||||||
|
Found[-1].List.append(Line.strip()[2:].strip()) # a list under the previous key
|
||||||
|
elif Line.strip():
|
||||||
|
return None
|
||||||
|
return Parsed(Found, EndLine, len("\n".join(Lines[:EndLine + 1])))
|
||||||
|
|
||||||
|
|
||||||
|
def ToggleLine(Text: str, LineIndex: int) -> str:
|
||||||
|
"""Flips a task's [ ]/[x], or a true/false property, on one line."""
|
||||||
|
Lines = Text.split("\n")
|
||||||
|
if not 0 <= LineIndex < len(Lines):
|
||||||
|
return Text
|
||||||
|
Line = Lines[LineIndex]
|
||||||
|
Task = TaskLine.match(Line)
|
||||||
|
if Task:
|
||||||
|
Mark = "x" if Task.group(2) == " " else " "
|
||||||
|
Lines[LineIndex] = f"{Task.group(1)}[{Mark}]{Line[Task.end():]}"
|
||||||
|
else:
|
||||||
|
Lines[LineIndex] = re.sub(r"(:\s*)(true|false)(\s*)$",
|
||||||
|
lambda M: M.group(1) + ("false" if M.group(2).lower() == "true" else "true") + M.group(3),
|
||||||
|
Line, flags=re.IGNORECASE)
|
||||||
|
return "\n".join(Lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _Checkbox(Checked: bool, Line: int, Toggle: OnToggle) -> ft.Checkbox:
|
||||||
|
return ft.Checkbox(
|
||||||
|
value=Checked,
|
||||||
|
disabled=Toggle is None,
|
||||||
|
active_color=Palette.Mauve,
|
||||||
|
check_color=Palette.Base,
|
||||||
|
visual_density=ft.VisualDensity.COMPACT,
|
||||||
|
on_change=(lambda e, L=Line: Toggle(L)) if Toggle else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def PropertiesView(Found: Parsed, FontSize: int, MakeMarkdown: Callable[[str], ft.Control],
|
||||||
|
Toggle: OnToggle) -> ft.Control:
|
||||||
|
"""The panel. MakeMarkdown renders a value (links, bold)."""
|
||||||
|
Rows: list[ft.Control] = []
|
||||||
|
for Item in Found.Properties:
|
||||||
|
if BooleanValue.match(Item.Value):
|
||||||
|
Shown: ft.Control = _Checkbox(Item.Value.lower() == "true", Item.Line, Toggle)
|
||||||
|
elif Item.List:
|
||||||
|
Shown = ft.Row([ft.Container(ft.Text(Entry, color=Palette.Mauve, size=FontSize - 1), bgcolor=Palette.Surface0,
|
||||||
|
border_radius=4, padding=ft.Padding.symmetric(horizontal=6, vertical=1))
|
||||||
|
for Entry in Item.List], wrap=True, spacing=4, run_spacing=4)
|
||||||
|
elif Item.Value:
|
||||||
|
Shown = MakeMarkdown(Item.Value)
|
||||||
|
else:
|
||||||
|
Shown = ft.Text("Empty", color=Palette.Overlay0, italic=True, size=FontSize - 1)
|
||||||
|
Rows.append(ft.Row([
|
||||||
|
ft.Container(ft.Text(Item.Key, color=Palette.Overlay0, size=FontSize - 1, max_lines=1,
|
||||||
|
overflow=ft.TextOverflow.ELLIPSIS), expand=3),
|
||||||
|
ft.Container(Shown, expand=7, alignment=ft.Alignment.CENTER_LEFT),
|
||||||
|
], spacing=12, vertical_alignment=ft.CrossAxisAlignment.CENTER))
|
||||||
|
return ft.Container(
|
||||||
|
ft.Column([ft.Text("Properties", color=Palette.Overlay0, size=FontSize - 1), *Rows], spacing=2, tight=True),
|
||||||
|
bgcolor=Palette.Mantle,
|
||||||
|
border=ft.Border.all(1, Palette.Surface1),
|
||||||
|
border_radius=4,
|
||||||
|
padding=ft.Padding.symmetric(horizontal=12, vertical=8),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def WithCheckboxes(Text: str, FirstLine: int, MakeMarkdown: Callable[[str], ft.Control],
|
||||||
|
Toggle: OnToggle, FontSize: int) -> list[ft.Control]:
|
||||||
|
"""Markdown with its task lines as real checkboxes: the lines between
|
||||||
|
them render as usual. FirstLine is where Text starts in the note."""
|
||||||
|
Controls: list[ft.Control] = []
|
||||||
|
Pending: list[str] = []
|
||||||
|
InFence = False
|
||||||
|
|
||||||
|
def Flush() -> None:
|
||||||
|
if "".join(Pending).strip():
|
||||||
|
Controls.append(MakeMarkdown("\n".join(Pending)))
|
||||||
|
Pending.clear()
|
||||||
|
|
||||||
|
for Offset, Line in enumerate(Text.split("\n")):
|
||||||
|
if re.match(r"^ {0,3}(`{3,}|~{3,})", Line):
|
||||||
|
InFence = not InFence
|
||||||
|
Task = None if InFence else TaskLine.match(Line)
|
||||||
|
if Task is None:
|
||||||
|
Pending.append(Line)
|
||||||
|
continue
|
||||||
|
Flush()
|
||||||
|
Indent = len(Task.group(1)) - len(Task.group(1).lstrip())
|
||||||
|
Controls.append(ft.Row(
|
||||||
|
[_Checkbox(Task.group(2) != " ", FirstLine + Offset, Toggle),
|
||||||
|
ft.Container(MakeMarkdown(Line[Task.end():].strip() or " "), expand=True)],
|
||||||
|
spacing=4,
|
||||||
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
|
))
|
||||||
|
if Indent:
|
||||||
|
Controls[-1] = ft.Container(Controls[-1], padding=ft.Padding.only(left=Indent * 8))
|
||||||
|
Flush()
|
||||||
|
return Controls or [MakeMarkdown(Text)]
|
||||||
+30
-4
@@ -2,7 +2,10 @@
|
|||||||
EditorView/ImageView idea, with SaraView's View/Edit/Save toolbar).
|
EditorView/ImageView idea, with SaraView's View/Edit/Save toolbar).
|
||||||
|
|
||||||
- Markdown is rendered in View; the other text types are shown as
|
- Markdown is rendered in View; the other text types are shown as
|
||||||
highlighted code. On a server, ```mermaid blocks show as diagrams (Diagrams.py). Edit is a plain monospace field with an explicit Save
|
highlighted code. On a server, ```mermaid blocks show as diagrams (Diagrams.py),
|
||||||
|
and the properties block at the top as its panel (Properties.py).
|
||||||
|
Checkboxes (tasks, true/false properties) tick from View and save at once,
|
||||||
|
like the web view; in Live they leave the note unsaved, like typing. Edit is a plain monospace field with an explicit Save
|
||||||
(Ctrl+S). On a server, saving records line blame for you, like the web view.
|
(Ctrl+S). On a server, saving records line blame for you, like the web view.
|
||||||
- With Markdown editing set to Live (Settings > General), Markdown opens in
|
- With Markdown editing set to Live (Settings > General), Markdown opens in
|
||||||
LiveMarkdown instead: rendered, and click a block to edit it in place.
|
LiveMarkdown instead: rendered, and click a block to edit it in place.
|
||||||
@@ -21,6 +24,7 @@ import flet as ft
|
|||||||
from App import App
|
from App import App
|
||||||
from Core import ChipButton, CodeStyleSheet, HumanSize, MarkdownCodeTheme, MarkdownSheet, Palette
|
from Core import ChipButton, CodeStyleSheet, HumanSize, MarkdownCodeTheme, MarkdownSheet, Palette
|
||||||
from Diagrams import MaybeDiagram, WithDiagrams
|
from Diagrams import MaybeDiagram, WithDiagrams
|
||||||
|
from Properties import ParseProperties, PropertiesView, ToggleLine, WithCheckboxes
|
||||||
from LiveMarkdown import LiveMarkdown
|
from LiveMarkdown import LiveMarkdown
|
||||||
from Sources import Entry, SourceError, TextFileTypes
|
from Sources import Entry, SourceError, TextFileTypes
|
||||||
|
|
||||||
@@ -137,18 +141,30 @@ class FileView(ft.Container):
|
|||||||
Text = self.TextField.value or ""
|
Text = self.TextField.value or ""
|
||||||
Views: Optional[list[ft.Control]] = None
|
Views: Optional[list[ft.Control]] = None
|
||||||
if self.FileType == "markdown" and len(Text) <= HighlightCharLimit:
|
if self.FileType == "markdown" and len(Text) <= HighlightCharLimit:
|
||||||
|
FontSize = self.TheApp.Config.FontSize
|
||||||
|
Toggle = self._ToggleAndSave if self.TheApp.Source.CanWrite else None
|
||||||
|
Found = ParseProperties(Text)
|
||||||
|
BodyStart = Found.EndOffset if Found else 0
|
||||||
|
|
||||||
def MakeMarkdown(Part: str) -> ft.Control:
|
def MakeMarkdown(Part: str) -> ft.Control:
|
||||||
return ft.Markdown(
|
return ft.Markdown(
|
||||||
value=Part,
|
value=Part,
|
||||||
selectable=True,
|
selectable=True,
|
||||||
extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED,
|
extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED,
|
||||||
md_style_sheet=MarkdownSheet(self.TheApp.Config.FontSize),
|
md_style_sheet=MarkdownSheet(FontSize),
|
||||||
code_theme=MarkdownCodeTheme(),
|
code_theme=MarkdownCodeTheme(),
|
||||||
code_style_sheet=CodeStyleSheet(),
|
code_style_sheet=CodeStyleSheet(),
|
||||||
on_tap_link=self._OnTapLink,
|
on_tap_link=self._OnTapLink,
|
||||||
)
|
)
|
||||||
|
|
||||||
Views = WithDiagrams(self.TheApp, Text, self.TheApp.Config.FontSize, MakeMarkdown)
|
def MakeWithCheckboxes(Part: str, Start: int) -> ft.Control:
|
||||||
|
FirstLine = Text[:BodyStart + Start].count("\n")
|
||||||
|
Parts = WithCheckboxes(Part, FirstLine, MakeMarkdown, Toggle, FontSize)
|
||||||
|
return Parts[0] if len(Parts) == 1 else ft.Column(Parts, spacing=0, tight=True)
|
||||||
|
|
||||||
|
Views = WithDiagrams(self.TheApp, Text[BodyStart:], FontSize, MakeWithCheckboxes)
|
||||||
|
if Found and Found.Properties:
|
||||||
|
Views.insert(0, PropertiesView(Found, FontSize, MakeMarkdown, Toggle))
|
||||||
View: ft.Control = Views[0]
|
View: ft.Control = Views[0]
|
||||||
elif self.FileType in ("config", "html") and len(Text) <= HighlightCharLimit:
|
elif self.FileType in ("config", "html") and len(Text) <= HighlightCharLimit:
|
||||||
Extension = "." + self.Item.Name.rsplit(".", 1)[-1].lower()
|
Extension = "." + self.Item.Name.rsplit(".", 1)[-1].lower()
|
||||||
@@ -203,6 +219,14 @@ class FileView(ft.Container):
|
|||||||
if Target.startswith(("http://", "https://", "mailto:")):
|
if Target.startswith(("http://", "https://", "mailto:")):
|
||||||
self.TheApp.Page.run_task(ft.UrlLauncher().launch_url, Target)
|
self.TheApp.Page.run_task(ft.UrlLauncher().launch_url, Target)
|
||||||
|
|
||||||
|
def _ToggleAndSave(self, Line: int) -> None:
|
||||||
|
"""A checkbox ticked in View: flip its line and save, like the web view."""
|
||||||
|
self.TextField.value = ToggleLine(self.TextField.value or "", Line)
|
||||||
|
self._OnTextChanged()
|
||||||
|
self.Save(Quiet=True)
|
||||||
|
self.Body.content = self._RenderedView()
|
||||||
|
self.Body.update()
|
||||||
|
|
||||||
# --- Editing ----------------------------------------------------------
|
# --- Editing ----------------------------------------------------------
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -273,7 +297,8 @@ class FileView(ft.Container):
|
|||||||
if self.IsText and not self.ToggleButton.disabled:
|
if self.IsText and not self.ToggleButton.disabled:
|
||||||
self._ToggleEdit()
|
self._ToggleEdit()
|
||||||
|
|
||||||
def Save(self) -> None:
|
def Save(self, Quiet: bool = False) -> None:
|
||||||
|
"""Quiet: no "Saved" notice (a ticked checkbox saves itself)."""
|
||||||
if not self.CanSave:
|
if not self.CanSave:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -287,6 +312,7 @@ class FileView(ft.Container):
|
|||||||
self.StatusText.value = "Saved."
|
self.StatusText.value = "Saved."
|
||||||
self.SaveButton.update()
|
self.SaveButton.update()
|
||||||
self.StatusText.update()
|
self.StatusText.update()
|
||||||
|
if not Quiet:
|
||||||
self.TheApp.Notify(f"Saved {self.Item.Name}.", "success")
|
self.TheApp.Notify(f"Saved {self.Item.Name}.", "success")
|
||||||
|
|
||||||
def RequestClose(self, Then: Optional[Callable[[], None]] = None) -> None:
|
def RequestClose(self, Then: Optional[Callable[[], None]] = None) -> None:
|
||||||
|
|||||||
Reference in new issue
Block a user