"""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, FolderIconBand, InstalledFontFamilies, LoadSharedSettings, Palette, SharedSettings, ) from FolderIcons import FolderIconChoices, FolderIconLabelOverrides # 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 ) # Folder Icons (Features: "turning on different variants depending on # how full a directory is... add more... choose icons (or custom # images) for each"). WorkingFolderBands is edited live in the dialog; # only committed to Config.FolderIconBands in DoSave, same as every # other field here. One band (the default) behaves exactly like # today's plain folder icon everywhere — nothing changes until a # second variant gets added. FolderThresholdField = ft.TextField( value=str(Config.FolderFullnessThreshold), label="Items counted as 100% full", keyboard_type=ft.KeyboardType.NUMBER, ) WorkingFolderBands: list[dict] = [ {"Icon": B.Icon, "ImagePath": B.ImagePath} for B in (Config.FolderIconBands or [FolderIconBand()]) ] # Live widget refs, one (IconDropdown, ImageField) pair per currently- # displayed row — read from directly at save time (and before any # Add/Remove rebuild) rather than syncing via on_change, since Dropdown # only exposes on_select (a different event) and TextField already # keeps its own .value current without any handler needed. FolderBandWidgets: list[tuple[ft.Dropdown, ft.TextField]] = [] FolderBandsColumn = ft.Column(spacing=8) def _FolderIconLabel(Name: str) -> str: return FolderIconLabelOverrides.get(Name, Name.split("_")[0].title()) def _SyncWorkingFolderBands() -> None: for Index, (IconDropdown, ImageField) in enumerate(FolderBandWidgets): WorkingFolderBands[Index] = { "Icon": IconDropdown.value or "FOLDER_OUTLINED", "ImagePath": (ImageField.value or "").strip(), } def RebuildFolderBandsColumn(DoUpdate: bool = True) -> None: FolderBandWidgets.clear() Count = len(WorkingFolderBands) Rows: list[ft.Control] = [] for Index, BandData in enumerate(WorkingFolderBands): Low = round(Index / Count * 100) High = round((Index + 1) / Count * 100) def OnRemove(e, I=Index) -> None: if len(WorkingFolderBands) > 1: _SyncWorkingFolderBands() WorkingFolderBands.pop(I) RebuildFolderBandsColumn() IconDropdown = ft.Dropdown( value=BandData["Icon"], options=[ ft.DropdownOption(key=Name, text=_FolderIconLabel(Name), leading_icon=getattr(ft.Icons, Name)) for Name in FolderIconChoices ], width=150, ) ImageField = ft.TextField(value=BandData["ImagePath"], label="Or custom image path", expand=True) FolderBandWidgets.append((IconDropdown, ImageField)) RowItems: list[ft.Control] = [ ft.Text(f"{Low}-{High}%", width=55, color=Palette.Overlay0), IconDropdown, ImageField, ] if Count > 1: RowItems.append( ft.IconButton(icon=ft.Icons.DELETE_OUTLINE, tooltip="Remove this variant", on_click=OnRemove) ) Rows.append(ft.Row(RowItems, spacing=8)) FolderBandsColumn.controls = Rows if DoUpdate: FolderBandsColumn.update() def AddFolderBand(e=None) -> None: _SyncWorkingFolderBands() WorkingFolderBands.append({"Icon": "FOLDER_OUTLINED", "ImagePath": ""}) RebuildFolderBandsColumn() RebuildFolderBandsColumn(DoUpdate=False) # dialog isn't shown yet AddFolderBandButton = ft.OutlinedButton( content="Add Variant", icon=ft.Icons.ADD, style=ChipButtonStyle(), on_click=AddFolderBand, ) # 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, ) SearchResetOnNavigateSwitch = ft.Switch( label="Clear search when changing directory", value=Config.SearchResetOnNavigate ) 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) RefreshButtonSwitch = ft.Switch(label="Show Refresh button", value=Config.ShowRefreshButton) 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) SystemDriveSwitch = ft.Switch( label="Include the main drive (/) too (never ejectable)", value=Config.ShowSystemDrive ) AutoHideEmptyDevicesSwitch = ft.Switch( label="Hide the devices section when there's nothing plugged in", value=Config.AutoHideEmptyDevices ) 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", ) SaraSortdShowBannerSwitch = ft.Switch( label="Show the SaraSortd info banner above the file list", value=Config.SaraSortd.ShowBanner ) SaraSortdCondenseBannerSwitch = ft.Switch( label="Condense multiple patterns per destination (collapsible)", value=Config.SaraSortd.CondenseBanner ) 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, scroll=ft.ScrollMode.AUTO, ), "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, ), "Folder Icons": ft.Column( [ _SectionLabel( "One variant always shows the same icon (today's plain folder, unless changed " "below). Add more to vary by how full a directory is, like Windows' folder fill " "levels — item count only, never a slow recursive size walk." ), FolderThresholdField, FolderBandsColumn, AddFolderBandButton, ft.Divider(height=1, color=Palette.Surface1), _SectionLabel( 'Per-directory overrides (right-click a folder → Properties → "Save Icon") ' "always win over these." ), ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "Toolbar": ft.Column( [ ToolbarSwitch, BreadcrumbsSwitch, _SectionLabel("Position"), BreadcrumbsPositionPicker, ft.Divider(height=1, color=Palette.Surface1), NewDirectoryButtonSwitch, PasteButtonSwitch, RefreshButtonSwitch, ViewToggleButtonSwitch, OptionsButtonSwitch, ft.Divider(height=1, color=Palette.Surface1), ToolbarHamburgerSwitch, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "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, SystemDriveSwitch, AutoHideEmptyDevicesSwitch, 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, scroll=ft.ScrollMode.AUTO, ), "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, SearchResetOnNavigateSwitch, ft.Divider(height=1, color=Palette.Surface1), ShowSortSwitch, _SectionLabel('The sort control ("[Sort: Name] v") sits above Bookmarks in the sidebar.'), ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "SaraTag": ft.Column([SaraTagSwitch, SaraTagPathField], spacing=12, scroll=ft.ScrollMode.AUTO), "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.' ), ft.Divider(height=1, color=Palette.Surface1), SaraSortdShowBannerSwitch, SaraSortdCondenseBannerSwitch, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), } 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, scroll=ft.ScrollMode.AUTO) 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.SearchResetOnNavigate = SearchResetOnNavigateSwitch.value Config.ShowSort = ShowSortSwitch.value Config.BatchRenamePattern = BatchRenamePatternField.value.strip() or "~!^" Config.DeleteZipAfterExtract = DeleteZipSwitch.value Config.DefaultArchiveFormat = GetArchiveFormat() _SyncWorkingFolderBands() Config.FolderIconBands = [ FolderIconBand(Icon=B["Icon"] or "FOLDER_OUTLINED", ImagePath=B["ImagePath"]) for B in WorkingFolderBands ] Config.ShowNewDirectoryButton = NewDirectoryButtonSwitch.value Config.ShowPasteButton = PasteButtonSwitch.value Config.ShowRefreshButton = RefreshButtonSwitch.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.ShowSystemDrive = SystemDriveSwitch.value Config.AutoHideEmptyDevices = AutoHideEmptyDevicesSwitch.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() Config.SaraSortd.ShowBanner = SaraSortdShowBannerSwitch.value Config.SaraSortd.CondenseBanner = SaraSortdCondenseBannerSwitch.value 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"), (FolderThresholdField, "FolderFullnessThreshold"), ): 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( [ # STRETCH confirmed necessary, not assumed: without it, Row # doesn't bound CategorySidebar/DetailArea's height to the # dialog's own fixed height, so their scroll=AUTO had nothing # to actually scroll within — content just overflowed instead # (Bugs/Minor: "Settings should be scrollable... too many for # one window"). ft.Row([CategorySidebar, DetailArea], expand=True, vertical_alignment=ft.CrossAxisAlignment.STRETCH), 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), ], )