"""In-app preferences dialog (Features/Major: "Edit preferences in SaraFem"). Laid out as a category sidebar + detail panel (Features/Minor: "Make settings look good... sidebar with areas, then a much bigger main box"), with the left/right/top/bottom-style choices as icon-button pickers instead of text dropdowns ("some way to choose layout in a more graphical... way").""" from typing import Callable import flet as ft from Core import AppConfig, AvailableThemes, ChipButtonStyle, Palette # Settings Browser can already hot-apply live (dotfiles, breadcrumbs, preview, tags). # The window title bar, sidebar (bookmarks/filetree) layout, theme, and SaraTag's # dynamically-loaded module need a restart to take effect. RestartRequiredNote = "Window title bar, Theme, Sidebar and SaraTag settings need a restart to take effect." DialogWidth = 700 DialogHeight = 520 CategoryListWidth = 150 def _MinMaxRow(Label: str, MinValue: int, MaxValue: int) -> tuple[ft.Row, ft.TextField, ft.TextField]: MinField = ft.TextField(value=str(MinValue), label=f"{Label} min", keyboard_type=ft.KeyboardType.NUMBER, expand=True) MaxField = ft.TextField(value=str(MaxValue), label=f"{Label} max", keyboard_type=ft.KeyboardType.NUMBER, expand=True) return ft.Row([MinField, MaxField]), MinField, MaxField def _SectionLabel(Text: str) -> ft.Text: return ft.Text(Text, color=Palette.Overlay0, size=12) def _UnselectedPickerStyle() -> ft.ButtonStyle: return ft.ButtonStyle(color=Palette.Overlay0, shape=ft.RoundedRectangleBorder(radius=4)) def _OptionPicker(Options: list[tuple[str, str, str]], Initial: str) -> tuple[ft.Control, Callable[[], str]]: """A row of icon buttons standing in for a text dropdown — Options is a list of (key, label, Icons-attribute-name). Returns (control, get_value).""" State = {"value": Initial} Buttons: dict[str, ft.OutlinedButton] = {} def Select(Key: str) -> None: State["value"] = Key for OptionKey, Button in Buttons.items(): Button.style = ChipButtonStyle() if OptionKey == Key else _UnselectedPickerStyle() Row.update() for Key, Label, IconName in Options: Buttons[Key] = ft.OutlinedButton( content=Label, icon=getattr(ft.Icons, IconName), style=ChipButtonStyle() if Key == Initial else _UnselectedPickerStyle(), on_click=lambda e, k=Key: Select(k), ) Row = ft.Row(list(Buttons.values()), wrap=True) return Row, lambda: State["value"] def BuildSettingsDialog( Config: AppConfig, OnSave: Callable[[], None], OnDismiss: Callable[[], None], ) -> ft.AlertDialog: ThemeDropdown = ft.Dropdown( value=Config.Theme, label="Theme (shared across SaraSuite)", options=[ft.DropdownOption(key=Name, text=Name) for Name in AvailableThemes()], ) DotfilesSwitch = ft.Switch(label="Show dotfiles", value=Config.ShowDotfiles) TitleBarSwitch = ft.Switch(label="Hide window title bar", value=Config.HideWindowTitleBar) BatchRenamePatternField = ft.TextField( value=Config.BatchRenamePattern, label="Batch rename pattern (~ = name, ! = counter, ^ = extension)", ) DeleteZipSwitch = ft.Switch( label="Delete zip after extracting, by default", value=Config.DeleteZipAfterExtract ) ArchiveFormatPicker, GetArchiveFormat = _OptionPicker( [("zip", "Zip", "FOLDER_ZIP_OUTLINED"), ("targz", "Tar.gz", "ARCHIVE_OUTLINED")], Config.DefaultArchiveFormat, ) NewDirectoryButtonSwitch = ft.Switch(label="Show New Directory button", value=Config.ShowNewDirectoryButton) PasteButtonSwitch = ft.Switch(label="Show Paste button", value=Config.ShowPasteButton) ViewToggleButtonSwitch = ft.Switch(label="Show Grid/List button", value=Config.ShowViewToggleButton) OptionsButtonSwitch = ft.Switch(label="Show Options button", value=Config.ShowOptionsButton) ToolbarSwitch = ft.Switch(label="Show toolbar (breadcrumbs + buttons bar)", value=Config.ShowToolbar) BreadcrumbsSwitch = ft.Switch(label="Show breadcrumbs", value=Config.ShowBreadcrumbs) BreadcrumbsPositionPicker, GetBreadcrumbsPosition = _OptionPicker( [("top", "Top", "NORTH"), ("bottom", "Bottom", "SOUTH")], Config.BreadcrumbsPosition ) SidebarSwitch = ft.Switch(label="Show sidebar", value=Config.ShowSidebar) SidebarSidePicker, GetSidebarSide = _OptionPicker( [("left", "Left", "WEST"), ("right", "Right", "EAST")], Config.SidebarSide ) SidebarWidthField = ft.TextField( value=str(Config.SidebarWidth), label="Sidebar width", keyboard_type=ft.KeyboardType.NUMBER ) BookmarksSwitch = ft.Switch(label="Show bookmarks section", value=Config.ShowBookmarks) BookmarksHeightRow, BookmarksMinHeightField, BookmarksMaxHeightField = _MinMaxRow( "Bookmarks height", Config.BookmarksMinHeight, Config.BookmarksMaxHeight ) DevicesSwitch = ft.Switch(label="Show devices section", value=Config.ShowDevices) AutoMountSwitch = ft.Switch(label="Mount devices when plugged in", value=Config.AutoMountDevices) InternalDrivesSwitch = ft.Switch(label="Also show internal drives", value=Config.ShowInternalDrives) DevicesHeightRow, DevicesMinHeightField, DevicesMaxHeightField = _MinMaxRow( "Devices height", Config.DevicesMinHeight, Config.DevicesMaxHeight ) FiletreeSwitch = ft.Switch(label="Show filetree section", value=Config.ShowFiletree) FiletreeRootField = ft.TextField(value=Config.FiletreeRoot, label="Filetree root", hint_text="/") FiletreeHeightRow, FiletreeMinHeightField, FiletreeMaxHeightField = _MinMaxRow( "Filetree height", Config.FiletreeMinHeight, Config.FiletreeMaxHeight ) SplitSidebarEvenlySwitch = ft.Switch( label="Split Bookmarks/Filetree evenly (overrides the heights above)", value=Config.SplitBookmarksFiletreeEvenly, ) PreviewSwitch = ft.Switch(label="Show preview pane", value=Config.ShowPreview) PreviewSidePicker, GetPreviewSide = _OptionPicker( [("left", "Left", "WEST"), ("right", "Right", "EAST")], Config.PreviewSide ) PreviewWidthField = ft.TextField( value=str(Config.PreviewWidth), label="Preview width", keyboard_type=ft.KeyboardType.NUMBER ) PreviewHeightRow, PreviewMinHeightField, PreviewMaxHeightField = _MinMaxRow( "Preview height (only when Tags is under preview)", Config.PreviewMinHeight, Config.PreviewMaxHeight ) SplitEvenlySwitch = ft.Switch( label="Split Preview/Tags evenly (overrides the heights above)", value=Config.SplitPreviewTagsEvenly, ) TagsSwitch = ft.Switch(label="Show SaraTag tags", value=Config.ShowTags) TagsPositionPicker, GetTagsPosition = _OptionPicker( [ ("under_preview", "Under Preview", "VIEW_AGENDA_OUTLINED"), ("top", "Top", "NORTH"), ("bottom", "Bottom", "SOUTH"), ], Config.TagsPosition, ) TagsLayoutPicker, GetTagsLayout = _OptionPicker( [("flex", "Flex", "VIEW_STREAM_OUTLINED"), ("vertical", "Vertical", "VIEW_COLUMN_OUTLINED")], Config.TagsLayout, ) TagMinWidthField = ft.TextField( value=str(Config.TagMinWidth), label="Tag min width (0 = none)", keyboard_type=ft.KeyboardType.NUMBER ) TagMaxWidthField = ft.TextField( value=str(Config.TagMaxWidth), label="Tag max width (0 = none)", keyboard_type=ft.KeyboardType.NUMBER ) TagPanelHeightRow, TagPanelMinHeightField, TagPanelMaxHeightField = _MinMaxRow( "Tags box height", Config.TagPanelMinHeight, Config.TagPanelMaxHeight ) SaraTagSwitch = ft.Switch(label="Enable SaraTag integration", value=Config.SaraTag.Enabled) SaraTagPathField = ft.TextField( value=Config.SaraTag.CoreModulePath, label="SaraTag .saratag path", hint_text="/home/sarah/Applications/SaraTag/.saratag", ) Categories: dict[str, ft.Control] = { "General": ft.Column( [ ThemeDropdown, DotfilesSwitch, TitleBarSwitch, BatchRenamePatternField, DeleteZipSwitch, _SectionLabel("Default compress format"), ArchiveFormatPicker, ], spacing=12, ), "Toolbar": ft.Column( [ ToolbarSwitch, BreadcrumbsSwitch, _SectionLabel("Position"), BreadcrumbsPositionPicker, ft.Divider(height=1, color=Palette.Surface1), NewDirectoryButtonSwitch, PasteButtonSwitch, ViewToggleButtonSwitch, OptionsButtonSwitch, ], spacing=12, ), "Sidebar": ft.Column( [ SidebarSwitch, _SectionLabel("Side"), SidebarSidePicker, SidebarWidthField, ft.Divider(height=1, color=Palette.Surface1), BookmarksSwitch, BookmarksHeightRow, ft.Divider(height=1, color=Palette.Surface1), DevicesSwitch, AutoMountSwitch, InternalDrivesSwitch, DevicesHeightRow, ft.Divider(height=1, color=Palette.Surface1), FiletreeSwitch, FiletreeRootField, FiletreeHeightRow, ft.Divider(height=1, color=Palette.Surface1), SplitSidebarEvenlySwitch, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "Preview": ft.Column( [ PreviewSwitch, _SectionLabel("Side"), PreviewSidePicker, PreviewWidthField, PreviewHeightRow, ], spacing=12, ), "Tags": ft.Column( [ TagsSwitch, _SectionLabel("Position"), TagsPositionPicker, _SectionLabel("Layout"), TagsLayoutPicker, TagMinWidthField, TagMaxWidthField, TagPanelHeightRow, SplitEvenlySwitch, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "SaraTag": ft.Column([SaraTagSwitch, SaraTagPathField], spacing=12), } DetailArea = ft.Container(Categories["General"], expand=True, padding=ft.Padding.only(left=12)) CategoryTiles: dict[str, ft.ListTile] = {} def SelectCategory(Name: str) -> None: for CategoryName, Tile in CategoryTiles.items(): Tile.selected = CategoryName == Name DetailArea.content = Categories[Name] DetailArea.update() CategoryListView.update() for Name in Categories: CategoryTiles[Name] = ft.ListTile( title=Name, selected=Name == "General", selected_tile_color=Palette.Surface1, selected_color=Palette.Mauve, on_click=lambda e, n=Name: SelectCategory(n), ) CategoryListView = ft.Column(list(CategoryTiles.values()), spacing=0) CategorySidebar = ft.Container( CategoryListView, width=CategoryListWidth, bgcolor=Palette.Surface0, border_radius=4, padding=4 ) def DoSave(e=None) -> None: Config.Theme = ThemeDropdown.value Config.ShowDotfiles = DotfilesSwitch.value Config.HideWindowTitleBar = TitleBarSwitch.value Config.BatchRenamePattern = BatchRenamePatternField.value.strip() or "~!^" Config.DeleteZipAfterExtract = DeleteZipSwitch.value Config.DefaultArchiveFormat = GetArchiveFormat() Config.ShowNewDirectoryButton = NewDirectoryButtonSwitch.value Config.ShowPasteButton = PasteButtonSwitch.value Config.ShowViewToggleButton = ViewToggleButtonSwitch.value Config.ShowOptionsButton = OptionsButtonSwitch.value Config.ShowToolbar = ToolbarSwitch.value Config.ShowBreadcrumbs = BreadcrumbsSwitch.value Config.BreadcrumbsPosition = GetBreadcrumbsPosition() Config.ShowSidebar = SidebarSwitch.value Config.SidebarSide = GetSidebarSide() Config.ShowBookmarks = BookmarksSwitch.value Config.ShowDevices = DevicesSwitch.value Config.AutoMountDevices = AutoMountSwitch.value Config.ShowInternalDrives = InternalDrivesSwitch.value Config.ShowFiletree = FiletreeSwitch.value Config.SplitBookmarksFiletreeEvenly = SplitSidebarEvenlySwitch.value Config.FiletreeRoot = FiletreeRootField.value.strip() or "/" Config.PreviewSide = GetPreviewSide() Config.ShowPreview = PreviewSwitch.value Config.SplitPreviewTagsEvenly = SplitEvenlySwitch.value Config.TagsPosition = GetTagsPosition() Config.TagsLayout = GetTagsLayout() Config.ShowTags = TagsSwitch.value Config.SaraTag.Enabled = SaraTagSwitch.value Config.SaraTag.CoreModulePath = SaraTagPathField.value.strip() for Field, TargetAttr in ( (SidebarWidthField, "SidebarWidth"), (BookmarksMinHeightField, "BookmarksMinHeight"), (BookmarksMaxHeightField, "BookmarksMaxHeight"), (DevicesMinHeightField, "DevicesMinHeight"), (DevicesMaxHeightField, "DevicesMaxHeight"), (FiletreeMinHeightField, "FiletreeMinHeight"), (FiletreeMaxHeightField, "FiletreeMaxHeight"), (PreviewWidthField, "PreviewWidth"), (PreviewMinHeightField, "PreviewMinHeight"), (PreviewMaxHeightField, "PreviewMaxHeight"), (TagMinWidthField, "TagMinWidth"), (TagMaxWidthField, "TagMaxWidth"), (TagPanelMinHeightField, "TagPanelMinHeight"), (TagPanelMaxHeightField, "TagPanelMaxHeight"), ): try: setattr(Config, TargetAttr, int(Field.value)) except ValueError: pass # keep the previous value rather than crash on bad input Config.Save() OnSave() return ft.AlertDialog( title="Options", content=ft.Column( [ ft.Row([CategorySidebar, DetailArea], expand=True), ft.Divider(height=1, color=Palette.Surface1), ft.Text(RestartRequiredNote, color=Palette.Overlay0, size=12), ], width=DialogWidth, height=DialogHeight, ), actions=[ ft.OutlinedButton(content="Cancel", style=ChipButtonStyle(), on_click=lambda e: OnDismiss()), ft.OutlinedButton(content="Save", style=ChipButtonStyle(), on_click=DoSave), ], )