"""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 pathlib import Path from typing import Callable import flet as ft from Core import ( AppConfig, AvailableThemes, ChipButtonStyle, InstalledFontFamilies, LoadSharedSettings, Palette, SharedSettings, ) # 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, Font 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], OnExport: Callable[[str], None], OnImport: Callable[[str], None], ) -> ft.AlertDialog: Themes = AvailableThemes() Fonts = InstalledFontFamilies() def ThemePicker(Value: str) -> ft.Dropdown: return ft.Dropdown( value=Value, label="Theme", options=[ft.DropdownOption(key=Name, text=Name) for Name in Themes], ) def FontPicker(Value: str) -> ft.Dropdown: # Installed fonts plus the bundled one; type to filter the long list. Names = Fonts if Value in Fonts else [Value] + Fonts return ft.Dropdown( value=Value, label="Font", editable=True, enable_filter=True, menu_height=320, options=[ft.DropdownOption(key=Name, text=Name) for Name in Names], ) # "SaraSuite" tab: the shared settings every SaraSuite program reads from # ~/.config/SaraSuite/Config.toml, and whether SaraFem follows them. Shared = LoadSharedSettings(Config) UseSharedSwitch = ft.Switch(label="SaraFem uses these shared settings", value=Config.UseSharedSettings) SharedThemeDropdown = ThemePicker(Shared.Theme) SharedFontDropdown = FontPicker(Shared.FontFamily) SharedTitleBarSwitch = ft.Switch(label="Hide window title bar", value=Shared.HideWindowTitleBar) # Export/import as one portable file (Features/Major) — separate from # the shared settings above: that's the always-on, automatic sync every # SaraSuite program reads on startup, this is a one-off snapshot you # choose a path for, to back up or carry to another machine. SettingsFileField = ft.TextField( value=str(Path.home() / "SaraFem-Settings.toml"), label="Settings file path" ) # "Middle" tab: the main file-list pane itself, not the panels around it. ViewModePicker, GetViewMode = _OptionPicker( [("list", "List", "VIEW_LIST_OUTLINED"), ("grid", "Grid", "GRID_VIEW_OUTLINED")], Config.ViewMode ) ListDescriptionSwitch = ft.Switch(label="Description", value=Config.ListShowDescription) ListSizeSwitch = ft.Switch(label="Size", value=Config.ListShowSize) ListModifiedSwitch = ft.Switch(label="Modified", value=Config.ListShowModified) ListOwnerSwitch = ft.Switch(label="Owner", value=Config.ListShowOwner) ListCreatedSwitch = ft.Switch(label="Created", value=Config.ListShowCreated) ListContentSwitch = ft.Switch( label="Content (item count / line count / image size)", value=Config.ListShowContent ) ListAccessSwitch = ft.Switch(label="Access (rwx)", value=Config.ListShowAccess) GridIconSizeField = ft.TextField( value=str(Config.GridIconSize), label="Grid icon size", keyboard_type=ft.KeyboardType.NUMBER ) GridNamePositionPicker, GetGridNamePosition = _OptionPicker( [("below", "Below", "SOUTH"), ("above", "Above", "NORTH")], "below" if Config.GridNameBelowIcon else "above", ) ListIconSizeField = ft.TextField( value=str(Config.ListIconSize), label="List icon size", keyboard_type=ft.KeyboardType.NUMBER ) FontSizeField = ft.TextField( value=str(Config.FontSize), label="Name font size", keyboard_type=ft.KeyboardType.NUMBER ) ToolbarHamburgerSwitch = ft.Switch( label="Collapse toolbar buttons into one menu", value=Config.ToolbarHamburger ) # SaraFem's own values ("General" tab), only used with shared settings off. ThemeDropdown = ThemePicker(Config.Theme) FontDropdown = FontPicker(Config.FontFamily) TitleBarSwitch = ft.Switch(label="Hide window title bar", value=Config.HideWindowTitleBar) OwnAppearance = ft.Column( [ _SectionLabel("SaraFem's own appearance, used when shared settings are off (SaraSuite tab)"), ThemeDropdown, FontDropdown, TitleBarSwitch, ], spacing=12, disabled=Config.UseSharedSettings, opacity=0.5 if Config.UseSharedSettings else 1, ) def OnUseSharedChanged(e) -> None: OwnAppearance.disabled = UseSharedSwitch.value OwnAppearance.opacity = 0.5 if UseSharedSwitch.value else 1 OwnAppearance.update() UseSharedSwitch.on_change = OnUseSharedChanged DotfilesSwitch = ft.Switch(label="Show dotfiles", value=Config.ShowDotfiles) OpenFilesInAppSwitch = ft.Switch( label="Open text/config/markup files in SaraFem's own viewer, not your default app", value=Config.OpenFilesInApp, ) ShowSearchSwitch = ft.Switch(label="Enable search (Ctrl+F)", value=Config.ShowSearch) SearchAlwaysVisibleSwitch = ft.Switch( label="Always show the search bar (off = only on Ctrl+F, Escape to dismiss)", value=Config.SearchAlwaysVisible, ) ShowSortSwitch = ft.Switch(label="Enable sort control in the sidebar", value=Config.ShowSort) 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", ) SaraSortdSwitch = ft.Switch(label="Enable SaraSortd integration", value=Config.SaraSortd.Enabled) SaraSortdPathField = ft.TextField( value=Config.SaraSortd.GlobalConfPath, label="SaraSortd GlobalConf.toml path", hint_text="/home/sarah/Applications/SaraSortd/GlobalConf.toml", ) Categories: dict[str, ft.Control] = { "SaraSuite": ft.Column( [ _SectionLabel("Shared by every SaraSuite program (~/.config/SaraSuite/Config.toml). " "Other programs pick changes up the next time they start."), UseSharedSwitch, ft.Divider(height=1, color=Palette.Surface1), SharedThemeDropdown, SharedFontDropdown, SharedTitleBarSwitch, ft.Divider(height=1, color=Palette.Surface1), _SectionLabel("Export/import settings as one file — this tab's shared " "settings plus everything else in Options"), SettingsFileField, ft.Row( [ ft.OutlinedButton( content="Export", style=ChipButtonStyle(), on_click=lambda e: OnExport(SettingsFileField.value.strip()), ), ft.OutlinedButton( content="Import", style=ChipButtonStyle(), on_click=lambda e: OnImport(SettingsFileField.value.strip()), ), ] ), ], spacing=12, ), "General": ft.Column( [ DotfilesSwitch, OpenFilesInAppSwitch, BatchRenamePatternField, DeleteZipSwitch, _SectionLabel("Default compress format"), ArchiveFormatPicker, ft.Divider(height=1, color=Palette.Surface1), OwnAppearance, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "Middle": ft.Column( [ _SectionLabel("View"), ViewModePicker, ft.Divider(height=1, color=Palette.Surface1), _SectionLabel("List columns (besides Name)"), ListDescriptionSwitch, ListSizeSwitch, ListModifiedSwitch, ListOwnerSwitch, ListCreatedSwitch, ListContentSwitch, ListAccessSwitch, ft.Divider(height=1, color=Palette.Surface1), _SectionLabel("Font and icon sizes"), FontSizeField, ListIconSizeField, ft.Divider(height=1, color=Palette.Surface1), _SectionLabel("Grid"), GridIconSizeField, _SectionLabel("Name position"), GridNamePositionPicker, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "Toolbar": ft.Column( [ ToolbarSwitch, BreadcrumbsSwitch, _SectionLabel("Position"), BreadcrumbsPositionPicker, ft.Divider(height=1, color=Palette.Surface1), NewDirectoryButtonSwitch, PasteButtonSwitch, ViewToggleButtonSwitch, OptionsButtonSwitch, ft.Divider(height=1, color=Palette.Surface1), ToolbarHamburgerSwitch, ], 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, ), "Search & Sort": ft.Column( [ ShowSearchSwitch, SearchAlwaysVisibleSwitch, ft.Divider(height=1, color=Palette.Surface1), ShowSortSwitch, _SectionLabel('The sort control ("[Sort: Name] v") sits above Bookmarks in the sidebar.'), ], spacing=12, ), "SaraTag": ft.Column([SaraTagSwitch, SaraTagPathField], spacing=12), "SaraSortd": ft.Column( [ SaraSortdSwitch, SaraSortdPathField, _SectionLabel( 'Shows what SaraSortd does with the directory you\'re browsing, e.g. ' '"*.png -> ~/Pictures" for a watched input directory, or ' '"*.png <- ~/Downloads" for one of its output directories.' ), ], 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.ViewMode = GetViewMode() Config.ListShowDescription = ListDescriptionSwitch.value Config.ListShowSize = ListSizeSwitch.value Config.ListShowModified = ListModifiedSwitch.value Config.ListShowOwner = ListOwnerSwitch.value Config.ListShowCreated = ListCreatedSwitch.value Config.ListShowContent = ListContentSwitch.value Config.ListShowAccess = ListAccessSwitch.value Config.GridNameBelowIcon = GetGridNamePosition() == "below" Config.ToolbarHamburger = ToolbarHamburgerSwitch.value Config.UseSharedSettings = UseSharedSwitch.value Config.Theme = ThemeDropdown.value Config.FontFamily = FontDropdown.value or Config.FontFamily Config.HideWindowTitleBar = TitleBarSwitch.value SharedSettings( Theme=SharedThemeDropdown.value, FontFamily=SharedFontDropdown.value or Shared.FontFamily, HideWindowTitleBar=SharedTitleBarSwitch.value, ).Save() Config.ShowDotfiles = DotfilesSwitch.value Config.OpenFilesInApp = OpenFilesInAppSwitch.value Config.ShowSearch = ShowSearchSwitch.value Config.SearchAlwaysVisible = SearchAlwaysVisibleSwitch.value Config.ShowSort = ShowSortSwitch.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() Config.SaraSortd.Enabled = SaraSortdSwitch.value Config.SaraSortd.GlobalConfPath = SaraSortdPathField.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"), (GridIconSizeField, "GridIconSize"), (ListIconSizeField, "ListIconSize"), (FontSizeField, "FontSize"), ): 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), ], )