"""In-app text/config/markup viewer and editor (Features/Major: "Different display in middle" — the code-editor part). Fills Browser's file-area Container in place of the entry list while a file is open. Flet 1.0.1 has no CodeMirror-equivalent rich editor widget and no embeddable WebView, so this mirrors SaraView's View/Edit split as closely as Flet actually allows: View mode gets real syntax highlighting by wrapping the content in a fenced code block and rendering it read-only via ft.Markdown (which ships GitHub-style highlight.js theming); Edit mode is a plain monospace TextField — no highlighting while typing — with an explicit Save.""" import re from pathlib import Path from typing import Callable, Optional import flet as ft from Core import ChipButtonStyle, Palette LanguageByExtension = { ".py": "python", ".json": "json", ".toml": "toml", ".yaml": "yaml", ".yml": "yaml", ".js": "javascript", ".ts": "typescript", ".css": "css", ".html": "xml", ".htm": "xml", ".xml": "xml", ".sh": "bash", ".fish": "bash", ".md": "markdown", ".ipynb": "json", ".cfg": "ini", ".ini": "ini", ".csv": "", ".log": "", ".scad": "", } # Above this, re-parsing the whole file into a highlighted markdown block on # every render gets slow — large logs stay readable (unhighlighted) instead. HighlightCharLimit = 500_000 def LanguageFor(EntryPath: Path) -> str: return LanguageByExtension.get(EntryPath.suffix.lower(), "") def FencedBlock(Content: str, Language: str) -> str: """A fence long enough that no run of backticks already in Content can prematurely close it (CommonMark: closing fence >= opening fence length).""" LongestRun = max((len(Run) for Run in re.findall(r"`+", Content)), default=0) Fence = "`" * max(LongestRun + 1, 3) return f"{Fence}{Language}\n{Content}\n{Fence}" class EditorView(ft.Container): def __init__( self, EntryPath: Path, FontFamily: str, OnClose: Callable[[], None], OnNotify: Callable[[str, str], None], OnFocusChange: Optional[Callable[[bool], None]] = None, ): self.EntryPath = EntryPath self.OnClose = OnClose self.OnNotify = OnNotify # Browser's Enter/Backspace/Delete/etc shortcuts are a page-level # keydown listener (see Browser._OnKeyboardEvent), not scoped to # whatever has focus — without this, typing in the TextField below # was also navigating directories (Backspace) or re-opening the file # (Enter) out from under you. Same ShortcutsPaused mechanism Picker.py # already uses for its own inline (non-dialog) text field. self.OnFocusChange = OnFocusChange self.Language = LanguageFor(EntryPath) try: self.OriginalContent = EntryPath.read_text(errors="replace") self.ReadError: Optional[str] = None except OSError as Error: self.OriginalContent = "" self.ReadError = str(Error) self.IsEditing = False self.IsDirty = False self.SkipHighlight = len(self.OriginalContent) > HighlightCharLimit CodeFont = ft.TextStyle(font_family=FontFamily, size=13) self.TextField = ft.TextField( value=self.OriginalContent, multiline=True, expand=True, border=ft.InputBorder.NONE, text_style=CodeFont, on_change=self._OnTextChanged, on_focus=lambda e: self._SetFocused(True), on_blur=lambda e: self._SetFocused(False), ) self.MarkdownView = ft.Markdown( value=FencedBlock(self.OriginalContent, self.Language), selectable=True, extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED, code_theme=ft.MarkdownCodeTheme.ATOM_ONE_DARK, ) self.PlainView = ft.Text(self.OriginalContent, selectable=True, style=CodeFont) self.SaveButton = ft.OutlinedButton( content="Save", style=ChipButtonStyle(), on_click=self._Save, disabled=True ) self.ToggleButton = ft.OutlinedButton(content="Edit", style=ChipButtonStyle(), on_click=self._ToggleEdit) self.StatusText = ft.Text("", color=Palette.Overlay0, size=12) Toolbar = ft.Row( [ ft.IconButton( icon=ft.Icons.ARROW_BACK, tooltip="Back to file list", on_click=lambda e: self.OnClose() ), ft.Text(EntryPath.name, weight=ft.FontWeight.BOLD, expand=True), self.StatusText, self.ToggleButton, self.SaveButton, ], spacing=8, ) if self.ReadError: BodyContent: ft.Control = ft.Text(f"Couldn't read this file: {self.ReadError}", color=Palette.Red) elif self.SkipHighlight: BodyContent = self.PlainView else: BodyContent = self.MarkdownView self.Body = ft.Container(content=BodyContent, expand=True, padding=12) super().__init__( content=ft.Column( [Toolbar, ft.Divider(height=1, color=Palette.Surface1), self.Body], expand=True, spacing=8 ), expand=True, bgcolor=Palette.Base, padding=12, ) def _SetFocused(self, Focused: bool) -> None: if self.OnFocusChange is not None: self.OnFocusChange(Focused) def _OnTextChanged(self, Event: ft.ControlEvent) -> None: if not self.IsDirty: self.IsDirty = True self.SaveButton.disabled = False self.StatusText.value = "Unsaved changes" self.SaveButton.update() self.StatusText.update() def _ToggleEdit(self, Event: ft.ControlEvent) -> None: if self.ReadError: return self.IsEditing = not self.IsEditing if self.IsEditing: self.Body.content = self.TextField self.ToggleButton.content = "View" # Don't wait on Flet's own focus event for this — swapping the # TextField into Body.content isn't guaranteed to fire on_focus # (or fire it before the next keypress), so shortcuts would stay # live for a keystroke or two right after clicking Edit. self._SetFocused(True) else: # Re-render the highlighted view from whatever's currently typed # (even if unsaved), so Edit -> View -> Edit doesn't discard it. if not self.SkipHighlight: self.MarkdownView.value = FencedBlock(self.TextField.value, self.Language) else: self.PlainView.value = self.TextField.value self.Body.content = self.PlainView if self.SkipHighlight else self.MarkdownView self.ToggleButton.content = "Edit" # Same reasoning in reverse: the TextField leaving the tree isn't # guaranteed to fire on_blur, so explicitly un-pause here rather # than risk shortcuts staying dead after leaving Edit mode. self._SetFocused(False) self.Body.update() self.ToggleButton.update() def _Save(self, Event: ft.ControlEvent) -> None: try: self.EntryPath.write_text(self.TextField.value) except OSError as Error: self.OnNotify(f"Couldn't save {self.EntryPath.name}: {Error}", "error") return self.OriginalContent = self.TextField.value self.IsDirty = False self.SaveButton.disabled = True self.StatusText.value = "Saved." self.SaveButton.update() self.StatusText.update() self.OnNotify(f"Saved {self.EntryPath.name}.", "success")