Files
SaraFem/Settings.py
T
2026-09-24 22:03:58 +02:00

349 lines
14 KiB
Python

"""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),
],
)