Files
SaraFem/Settings.py
T

532 lines
23 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 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)
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,
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,
),
"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.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),
],
)