"""The file list and everything around it, laid out like SaraFem's Browser: breadcrumbs and the toolbar share one bar on top, the file list fills the rest, and an open file takes the list's place (Viewer.py). Right-click menus use SaraFem's one shared ContextMenu (see _RequestMenu).""" import asyncio import time from pathlib import Path from typing import Callable, Optional import flet as ft from App import App from Core import ChipButtonStyle, HumanSize, OpenExternally, Palette from Sources import BaseName, Entry, JoinPath, MoveDestination, ParentPath, SourceError from Viewer import FileView DoubleClickWindow = 0.4 # seconds FileIcons = { "markdown": ft.Icons.DESCRIPTION_OUTLINED, "text": ft.Icons.ARTICLE_OUTLINED, "config": ft.Icons.SETTINGS_OUTLINED, "html": ft.Icons.CODE, "image": ft.Icons.IMAGE_OUTLINED, "video": ft.Icons.MOVIE_OUTLINED, } TypeLabels = {"markdown": "Markdown", "text": "Text", "config": "Config", "html": "HTML", "image": "Image", "video": "Video"} class Browser(ft.Column): def __init__(self, TheApp: App, OnPathChanged: Callable[[Optional[str], str, bool], None], OnShowSettings: Callable[[], None], OnToggleSidebar: Callable[[], None]): super().__init__(expand=True, spacing=0, horizontal_alignment=ft.CrossAxisAlignment.STRETCH) self.TheApp = TheApp self.Config = TheApp.Config self.OnPathChanged = OnPathChanged self.OnShowSettings = OnShowSettings self.OnToggleSidebar = OnToggleSidebar # Root None: the list of roots (a server with more than one). Path is # the directory inside Root, "" for the root itself. self.Root: Optional[str] = None self.Path = "" self.Entries: list[Entry] = [] self.ListError = "" # why the current directory couldn't be listed self.SelectedName: Optional[str] = None self.EntryRows: dict[str, ft.Container] = {} self.BackStack: list[tuple[Optional[str], str]] = [] self.ForwardStack: list[tuple[Optional[str], str]] = [] self.OpenView: Optional[FileView] = None self.ShortcutsPaused = False # a text field has focus self.LastClickName: Optional[str] = None self.LastClickTime = 0.0 # Right-click menu state, see _RequestMenu. self.MenuOpen = False self.MenuOpenedAt = 0.0 self.PendingMenu: Optional[tuple[Optional[str], ft.Offset]] = None self.Breadcrumbs = ft.Row(spacing=0, wrap=True) self.NewFileButton = ft.OutlinedButton(content="New File", icon=ft.Icons.NOTE_ADD_OUTLINED, style=ChipButtonStyle(), on_click=lambda e: self._ShowNewDialog(False)) self.NewDirectoryButton = ft.OutlinedButton(content="New Directory", icon=ft.Icons.CREATE_NEW_FOLDER_OUTLINED, style=ChipButtonStyle(), on_click=lambda e: self._ShowNewDialog(True)) self.UploadButton = ft.OutlinedButton(content="Upload", icon=ft.Icons.UPLOAD_OUTLINED, style=ChipButtonStyle(), on_click=self._Upload) self.ConnectionButton = ft.PopupMenuButton( content=ft.Container( ft.Row([ft.Icon(ft.Icons.CLOUD_OUTLINED, size=16, color=Palette.Mauve), ft.Text("", size=13)], spacing=6, tight=True), padding=ft.Padding.symmetric(horizontal=10, vertical=6), border=ft.Border.all(1, Palette.Mauve), border_radius=4, bgcolor=Palette.Surface0, ), items=[], ) self.ActionsToolbar = ft.Row( [ self.NewFileButton, self.NewDirectoryButton, self.UploadButton, ft.IconButton(icon=ft.Icons.REFRESH, icon_color=Palette.Mauve, tooltip="Refresh (Ctrl+R)", on_click=lambda e: self.Refresh()), self.ConnectionButton, ft.IconButton(icon=ft.Icons.SETTINGS_OUTLINED, icon_color=Palette.Mauve, tooltip="Settings", on_click=lambda e: self.OnShowSettings()), ], spacing=8, tight=True, ) self.EntryList = ft.ListView(expand=True, spacing=2) # One shared popup host, opened programmatically (see _RequestMenu). # Its built-in triggers can't be switched off, so they're set to # LONG_PRESS with no items, which leaves them inert (SaraFem's fix for # the "right-click turns the whole app black" bug). self.ContextMenu = ft.ContextMenu( content=ft.Column([self._BuildListHeader(), self.EntryList], spacing=0, expand=True), expand=True, secondary_trigger=ft.ContextMenuTrigger.LONG_PRESS, tertiary_trigger=ft.ContextMenuTrigger.LONG_PRESS, on_select=self._OnMenuClosed, on_dismiss=self._OnMenuClosed, ) self.FileArea = ft.Container(expand=True) self.controls = [ # Unlike SaraFem's 50/50 split, the buttons take what they need # and the breadcrumbs get the rest (and wrap when it's tight). ft.Row([ft.Container(self.Breadcrumbs, padding=8, expand=True), ft.Container(self.ActionsToolbar, padding=8)]), ft.Divider(height=1, color=Palette.Surface1), self.FileArea, ] self._ShowList(Update=False) def did_mount(self) -> None: self.page.on_keyboard_event = self._OnKeyboardEvent # --- Source ----------------------------------------------------------- def SourceChanged(self) -> None: """The source was (re)connected, or someone logged in or out: what's visible may have changed, so start over from the first root.""" self._RefreshConnectionButton() self.BackStack.clear() self.ForwardStack.clear() self.OpenView = None self.ShortcutsPaused = False if self.TheApp.ConnectError: self.Root, self.Path, self.Entries = None, "", [] self._ShowList() return try: Roots = self.TheApp.Source.Roots() except SourceError as Error: self.TheApp.HandleError(Error) Roots = [] self.NavigateTo(Roots[0] if len(Roots) == 1 else None, "", RecordHistory=False) def _RefreshConnectionButton(self) -> None: TheApp = self.TheApp Server = TheApp.Server Label: ft.Text = self.ConnectionButton.content.content.controls[1] Icon: ft.Icon = self.ConnectionButton.content.content.controls[0] Items: list[ft.PopupMenuItem] = [] if Server is None: Label.value = "Local" Icon.icon = ft.Icons.COMPUTER Items.append(ft.PopupMenuItem(content=TheApp.Source.Describe(), disabled=True)) else: Label.value = (Server.User or {}).get("Username") or ("Offline" if TheApp.ConnectError else "Guest") Icon.icon = ft.Icons.CLOUD_OFF_OUTLINED if TheApp.ConnectError else ft.Icons.CLOUD_OUTLINED Items.append(ft.PopupMenuItem(content=Server.Url or "No server address", disabled=True)) if TheApp.ConnectError: Items.append(ft.PopupMenuItem(content="Reconnect", icon=ft.Icons.REFRESH, on_click=lambda e: TheApp.Connect())) elif Server.User is None: Items.append(ft.PopupMenuItem(content="Log in", icon=ft.Icons.LOGIN, on_click=lambda e: TheApp.ShowLoginDialog())) else: Items.append(ft.PopupMenuItem(content="Log out", icon=ft.Icons.LOGOUT, on_click=lambda e: TheApp.Logout())) Items.append(ft.PopupMenuItem(content="Connection settings", icon=ft.Icons.SETTINGS_OUTLINED, on_click=lambda e: self.OnShowSettings())) self.ConnectionButton.items = Items self.ConnectionButton.tooltip = TheApp.Source.Describe() CanWrite = TheApp.Source.CanWrite and not TheApp.ConnectError for Button in (self.NewFileButton, self.NewDirectoryButton, self.UploadButton): Button.disabled = not CanWrite Button.tooltip = None if CanWrite else "Log in to change files" # --- Navigation ------------------------------------------------------- def NavigateTo(self, Root: Optional[str], RelativePath: str, RecordHistory: bool = True) -> None: if self.OpenView is not None and self.OpenView.IsDirty: self.OpenView.RequestClose(Then=lambda: self.NavigateTo(Root, RelativePath, RecordHistory)) return if RecordHistory and (Root, RelativePath) != (self.Root, self.Path): self.BackStack.append((self.Root, self.Path)) self.ForwardStack.clear() self.Root, self.Path = Root, RelativePath self.SelectedName = None self.OpenView = None self.ShortcutsPaused = False self._LoadEntries() self._ShowList() self.OnPathChanged(Root, RelativePath, False) def GoBack(self) -> None: if self.BackStack: self.ForwardStack.append((self.Root, self.Path)) self.NavigateTo(*self.BackStack.pop(), RecordHistory=False) def GoForward(self) -> None: if self.ForwardStack: self.BackStack.append((self.Root, self.Path)) self.NavigateTo(*self.ForwardStack.pop(), RecordHistory=False) def GoUp(self) -> None: if self.Root is None: return if self.Path: self.NavigateTo(self.Root, ParentPath(self.Path)) elif len(self._Roots()) > 1: self.NavigateTo(None, "") def Refresh(self) -> None: if self.OpenView is not None: return # reloading would throw away edits; close the file first self._LoadEntries() self._ShowList() def _Roots(self) -> list[str]: try: return self.TheApp.Source.Roots() except SourceError: return [] def _LoadEntries(self) -> None: self.ListError = "" if self.TheApp.ConnectError: self.Entries = [] return try: if self.Root is None: self.Entries = [Entry(Name, True) for Name in self.TheApp.Source.Roots()] else: self.Entries = self.TheApp.Source.List(self.Root, self.Path) except SourceError as Error: self.Entries = [] self.ListError = str(Error) def _EntryPath(self, Name: str) -> tuple[str, str]: """(Root, Path) of an entry in the current directory.""" if self.Root is None: return Name, "" return self.Root, JoinPath(self.Path, Name) def _Find(self, Name: Optional[str]) -> Optional[Entry]: return next((Item for Item in self.Entries if Item.Name == Name), None) # --- Rendering -------------------------------------------------------- def _ShowList(self, Update: bool = True) -> None: self.Breadcrumbs.controls = self._BuildBreadcrumbs() if self.TheApp.ConnectError: self.FileArea.content = self._EmptyState( "Can't reach the server", self.TheApp.ConnectError, [("Reconnect", lambda e: self.TheApp.Connect()), ("Settings", lambda e: self.OnShowSettings())], ) else: self.EntryList.controls = self._BuildRows() self.FileArea.content = ft.GestureDetector( content=ft.Container(content=self.ContextMenu, bgcolor=Palette.Base, expand=True), on_tap=lambda e: self.Select(None), on_secondary_tap_down=lambda e: self._RequestMenu(None, e), ) if Update: self.update() def _EmptyState(self, Title: str, Detail: str, Buttons: list[tuple[str, Callable]]) -> ft.Control: return ft.Container( ft.Column( [ ft.Text(Title, color=Palette.Overlay0, size=16), ft.Text(Detail, color=Palette.Overlay0, size=12, text_align=ft.TextAlign.CENTER), ft.Row([ft.OutlinedButton(content=Text, style=ChipButtonStyle(), on_click=Handler) for Text, Handler in Buttons], alignment=ft.MainAxisAlignment.CENTER), ], horizontal_alignment=ft.CrossAxisAlignment.CENTER, tight=True, ), alignment=ft.Alignment.CENTER, expand=True, padding=24, ) def _BuildBreadcrumbs(self) -> list[ft.Control]: Segments: list[tuple[str, Optional[str], str]] = [] # (label, root, path) if self.Root is None or len(self._Roots()) > 1: Segments.append(("Roots", None, "")) if self.Root is not None: Segments.append((self.Root, self.Root, "")) Cumulative = "" for Part in [Part for Part in self.Path.split("/") if Part]: Cumulative = JoinPath(Cumulative, Part) Segments.append((Part, self.Root, Cumulative)) Controls: list[ft.Control] = [] for Index, (Label, Root, PathText) in enumerate(Segments): Controls.append(ft.TextButton(content=Label, on_click=lambda e, R=Root, P=PathText: self.NavigateTo(R, P))) if Index < len(Segments) - 1: Controls.append(ft.Text("/", color=Palette.Overlay0)) return Controls def _BuildListHeader(self) -> ft.Control: def Cell(Text: str, Width: Optional[int] = None, Expand: bool = False) -> ft.Container: return ft.Container(ft.Text(Text, size=12, color=Palette.Overlay0), width=Width, expand=Expand, padding=ft.Padding.only(left=12, right=8)) return ft.Container( ft.Row([Cell("Name", Expand=True), Cell("Type", 110), Cell("Size", 100), ft.Container(width=48)], spacing=0), padding=ft.Padding.symmetric(vertical=6), bgcolor=Palette.Mantle, border=ft.Border(bottom=ft.BorderSide(1, Palette.Surface1)), ) def _BuildRows(self) -> list[ft.Control]: self.EntryRows = {} if self.ListError: return [ft.Container(ft.Text(self.ListError, color=Palette.Red), padding=16)] if not self.Entries: return [ft.Container(ft.Text("This directory is empty.", color=Palette.Overlay0, italic=True), padding=16)] return [self._BuildRow(Item) for Item in self.Entries] def _BuildRow(self, Item: Entry) -> ft.Control: FontSize = self.Config.FontSize if self.Root is None: Icon, IconColor, TypeText = ft.Icons.STORAGE_OUTLINED, Palette.Mauve, "Root" elif Item.IsDirectory: Icon, IconColor, TypeText = ft.Icons.FOLDER_OUTLINED, Palette.Mauve, "Directory" else: Icon = FileIcons.get(Item.FileType or "", ft.Icons.INSERT_DRIVE_FILE_OUTLINED) IconColor = Palette.Text TypeText = TypeLabels.get(Item.FileType or "", "File") Row = ft.Container( content=ft.Row( [ ft.Container( ft.Row([ ft.Icon(Icon, size=FontSize + 6, color=IconColor), ft.Text(Item.Name, size=FontSize, color=Palette.Text, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1, expand=True), ], spacing=12), expand=True, padding=ft.Padding.symmetric(horizontal=12, vertical=8), ), ft.Container(ft.Text(TypeText, size=FontSize - 2, color=Palette.Overlay0), width=110, padding=ft.Padding.only(left=12)), ft.Container(ft.Text(HumanSize(Item.Size), size=FontSize - 2, color=Palette.Overlay0), width=100, padding=ft.Padding.only(left=12)), ft.PopupMenuButton(icon=ft.Icons.MORE_VERT, icon_color=Palette.Overlay0, items=self._MenuItems(Item.Name)), ], spacing=0, ), bgcolor=Palette.Surface0 if Item.Name == self.SelectedName else None, border_radius=4, ) self.EntryRows[Item.Name] = Row return ft.GestureDetector( content=Row, on_tap=lambda e, N=Item.Name: self._HandleTap(N), on_secondary_tap_down=lambda e, N=Item.Name: self._RequestMenu(N, e), ) # --- Selection and opening -------------------------------------------- def _HandleTap(self, Name: str) -> None: Now = time.monotonic() IsDoubleClick = Name == self.LastClickName and (Now - self.LastClickTime) < DoubleClickWindow self.LastClickName = None if IsDoubleClick else Name self.LastClickTime = Now if IsDoubleClick: self.OpenEntry(Name) else: self.Select(Name) def Select(self, Name: Optional[str]) -> None: if Name == self.SelectedName: return for RowName in (self.SelectedName, Name): if RowName in self.EntryRows: self.EntryRows[RowName].bgcolor = Palette.Surface0 if RowName == Name else None self.EntryRows[RowName].update() self.SelectedName = Name def OpenEntry(self, Name: str, InApp: Optional[bool] = None) -> None: Item = self._Find(Name) if Item is None: return Root, RelativePath = self._EntryPath(Name) if Item.IsDirectory: self.NavigateTo(Root, RelativePath) return if InApp is None: InApp = self.Config.OpenFilesInApp if not InApp: self._OpenExternally(Root, RelativePath) return self.OpenView = FileView( self.TheApp, Root, RelativePath, Item, OnClose=self._CloseFile, OnOpenExternally=lambda: self._OpenExternally(Root, RelativePath), OnDownload=(lambda: self._Download(Name)) if self.TheApp.Server else None, OnFocusChange=self._SetShortcutsPaused, ) self.FileArea.content = self.OpenView self.FileArea.update() def _CloseFile(self) -> None: self.OpenView = None self.ShortcutsPaused = False self._ShowList() def _OpenExternally(self, Root: str, RelativePath: str) -> None: try: if self.TheApp.Server: self.TheApp.Notify(f"Downloading {BaseName(RelativePath)}...", "info") OpenExternally(self.TheApp.Source.LocalFile(Root, RelativePath)) except SourceError as Error: self.TheApp.HandleError(Error, f"Couldn't open {BaseName(RelativePath)}") except OSError as Error: self.TheApp.Notify(f"Couldn't open {BaseName(RelativePath)}: {Error}", "error") # --- Menus ------------------------------------------------------------ def _MenuItems(self, Name: str) -> list[ft.PopupMenuItem]: """Shared by the triple-dot button and the right-click menu.""" if self.Root is None: return [ft.PopupMenuItem(content="Open", on_click=lambda e: self.OpenEntry(Name))] Item = self._Find(Name) CanWrite = self.TheApp.Source.CanWrite Items: list[ft.PopupMenuItem] = [] if Item is not None and not Item.IsDirectory: OtherWay = "Open Externally" if self.Config.OpenFilesInApp else "View in SaraView" Items += [ ft.PopupMenuItem(content="View" if self.Config.OpenFilesInApp else "Open Externally", on_click=lambda e: self.OpenEntry(Name)), ft.PopupMenuItem(content=OtherWay, on_click=lambda e: self.OpenEntry(Name, InApp=not self.Config.OpenFilesInApp)), ] else: Items.append(ft.PopupMenuItem(content="Open", on_click=lambda e: self.OpenEntry(Name))) if self.TheApp.Server: Items.append(ft.PopupMenuItem(content="Download", on_click=lambda e: self._Download(Name))) Items += [ ft.PopupMenuItem(content="Copy Path", on_click=lambda e: self._CopyPath(Name)), ft.PopupMenuItem(content="Rename / Move", disabled=not CanWrite, on_click=lambda e: self._ShowRenameDialog(Name)), ft.PopupMenuItem(content="Delete", disabled=not CanWrite, on_click=lambda e: self._ConfirmDelete(Name)), ] return Items def _EmptySpaceMenuItems(self) -> list[ft.PopupMenuItem]: CanWrite = self.TheApp.Source.CanWrite and self.Root is not None return [ ft.PopupMenuItem(content="New File", disabled=not CanWrite, on_click=lambda e: self._ShowNewDialog(False)), ft.PopupMenuItem(content="New Directory", disabled=not CanWrite, on_click=lambda e: self._ShowNewDialog(True)), ft.PopupMenuItem(content="Upload", disabled=not CanWrite, on_click=self._Upload), ft.PopupMenuItem(content="Refresh", on_click=lambda e: self.Refresh()), ] def _RequestMenu(self, Name: Optional[str], Event: ft.TapEvent) -> None: """Right-click on an entry (Name) or the background (None). An entry sits inside the background's GestureDetector and both report the click, so this waits a moment and opens one menu, the entry's winning (SaraFem's _RequestMenu).""" if self.MenuOpen: if time.monotonic() - self.MenuOpenedAt < 0.5: return self.MenuOpen = False First = self.PendingMenu is None if First or Name is not None: self.PendingMenu = (Name, Event.global_position) if First: self.page.run_task(self._OpenPendingMenu) async def _OpenPendingMenu(self) -> None: await asyncio.sleep(0.08) Name, Position = self.PendingMenu self.PendingMenu = None if Name is not None: self.Select(Name) self.ContextMenu.items = self._MenuItems(Name) if Name is not None else self._EmptySpaceMenuItems() self.ContextMenu.update() self.MenuOpen = True self.MenuOpenedAt = time.monotonic() try: await self.ContextMenu.open(global_position=Position) except Exception: self.MenuOpen = False raise def _OnMenuClosed(self, Event) -> None: if Event.button is None: # only menus opened through open() self.MenuOpen = False # --- File operations -------------------------------------------------- def _AfterChange(self) -> None: """Re-list, and let the sidebar tree catch up with moved directories.""" self.Refresh() self.OnPathChanged(self.Root, self.Path, True) def _ShowNewDialog(self, IsDirectory: bool) -> None: if self.Root is None: self.TheApp.Notify("Open a root first.", "warning") return Default = "New Directory" if IsDirectory else "New File.md" def Create(NewName: str) -> None: Root, RelativePath = self._EntryPath(NewName) try: self.TheApp.Source.Create(Root, RelativePath, IsDirectory) except SourceError as Error: self.TheApp.HandleError(Error, f"Couldn't create {NewName}") return self._AfterChange() if not IsDirectory: self.Select(NewName) # Unlike Rename, Create also takes the suggested name as it is. self.TheApp.AskName("New Directory" if IsDirectory else "New File", Default, len(Default.rsplit(".", 1)[0]), Create, RequireChange=False) def _ShowRenameDialog(self, Name: str) -> None: Item = self._Find(Name) Root, RelativePath = self._EntryPath(Name) SelectEnd = len(Name) if (Item and Item.IsDirectory) or "." not in Name else len(Name.rsplit(".", 1)[0]) def Rename(NewName: str) -> None: Destination = MoveDestination(RelativePath, NewName) if not Destination: self.TheApp.Notify("That would move it to the root itself.", "warning") return try: self.TheApp.Source.Move(Root, RelativePath, Destination) except SourceError as Error: self.TheApp.HandleError(Error, f"Couldn't rename {Name}") return self._AfterChange() self.TheApp.AskName("Rename", Name, SelectEnd, Rename, ActionText="Rename", Note='A "/" moves it, e.g. ../Archive/' + Name) def _ConfirmDelete(self, Name: str) -> None: Item = self._Find(Name) Root, RelativePath = self._EntryPath(Name) def Delete() -> None: try: self.TheApp.Source.Delete(Root, RelativePath) except SourceError as Error: self.TheApp.HandleError(Error, f"Couldn't delete {Name}") return self.SelectedName = None self._AfterChange() self.TheApp.Notify(f"Deleted {Name}.", "success") if not self.Config.ConfirmDelete: Delete() return What = "directory and everything in it" if Item and Item.IsDirectory else "file" Where = "moved to the trash" if self.TheApp.Server is None else "deleted from the server for good" self.TheApp.Confirm(f"Delete {Name}?", f"This {What} will be {Where}.", Delete) def _CopyPath(self, Name: str) -> None: Root, RelativePath = self._EntryPath(Name) Server = self.TheApp.Server Text = f"{Root}/{RelativePath}" if Server else str(self.TheApp.Source.LocalFile(Root, RelativePath)) self.page.run_task(ft.Clipboard().set, Text) self.TheApp.Notify(f"Copied {Text}", "success") async def _Upload(self, e=None) -> None: if self.Root is None or not self.TheApp.Source.CanWrite: return Picked = await self.TheApp.Picker.pick_files(dialog_title="Upload files", allow_multiple=True) Paths = [Path(File.path) for File in Picked or [] if File.path] if not Paths: return Saved, Failed = 0, [] Server = self.TheApp.Server for LocalFile in Paths: if Server: self.TheApp.Notify(f"Uploading {LocalFile.name} ({HumanSize(LocalFile.stat().st_size)})...", "info") try: await asyncio.to_thread(self.TheApp.Source.Upload, self.Root, self.Path, LocalFile) Saved += 1 except SourceError as Error: if Error.Status == 401: self.TheApp.HandleError(Error) return Failed.append(f"{LocalFile.name}: {Error}") except OSError as Error: Failed.append(f"{LocalFile.name}: {Error}") self.Refresh() if Failed: self.TheApp.Notify("Couldn't upload " + "; ".join(Failed), "error") else: self.TheApp.Notify(f"{'Uploaded' if Server else 'Copied in'} {Saved} file{'s' if Saved != 1 else ''}.", "success") def _Download(self, Name: str) -> None: self.page.run_task(self._DoDownload, Name) async def _DoDownload(self, Name: str) -> None: Server = self.TheApp.Server Item = self._Find(Name) or (self.OpenView.Item if self.OpenView else None) if Server is None: return Root, RelativePath = self._EntryPath(Name) FileName = f"{Name}.zip" if (Item and Item.IsDirectory) or self.Root is None else Name Chosen = await self.TheApp.Picker.save_file(dialog_title="Download to", file_name=FileName, initial_directory=str(Path.home() / "Downloads")) if not Chosen: return self.TheApp.Notify(f"Downloading {FileName}...", "info") try: await asyncio.to_thread(Server.Download, Root, RelativePath, Path(Chosen)) except SourceError as Error: self.TheApp.HandleError(Error, f"Couldn't download {Name}") return self.TheApp.Notify(f"Saved {Chosen}", "success") # --- Keyboard --------------------------------------------------------- def _SetShortcutsPaused(self, Paused: bool) -> None: self.ShortcutsPaused = Paused def _OnKeyboardEvent(self, Event: ft.KeyboardEvent) -> None: if self.TheApp.DialogOpen: return View = self.OpenView # These work while typing too. if Event.key == "S" and Event.ctrl and View is not None: View.Save() return if Event.key == "E" and Event.ctrl and View is not None: View.ToggleEdit() return if Event.key == "Escape" and View is not None: View.RequestClose() return if self.ShortcutsPaused: return if Event.key == "B" and Event.ctrl: self.OnToggleSidebar() elif Event.key == "R" and Event.ctrl: self.Refresh() elif Event.key == "Arrow Left" and Event.alt: self.GoBack() elif Event.key == "Arrow Right" and Event.alt: self.GoForward() elif View is not None: return # the rest act on the file list elif Event.key == "N" and Event.ctrl and Event.shift: self._ShowNewDialog(True) elif Event.key == "N" and Event.ctrl: self._ShowNewDialog(False) elif Event.key == "Backspace": self.GoUp() elif Event.key == "Escape": self.Select(None) elif self.SelectedName is None: return elif Event.key == "Enter": self.OpenEntry(self.SelectedName) elif Event.key == "F2" and self.TheApp.Source.CanWrite and self.Root is not None: self._ShowRenameDialog(self.SelectedName) elif Event.key == "Delete" and self.TheApp.Source.CanWrite and self.Root is not None: self._ConfirmDelete(self.SelectedName)