"""Launcher settings dialog, isolated from dashboard orchestration.""" from __future__ import annotations from typing import Any, Callable from ...paths import AppPaths from ...settings import Settings, ensure_settings, update_settings from ...tk_chrome import ( centered_window_geometry, create_window_chrome, show_themed_dialog, ) from ..viewmodels.dashboard import ( UI_SCALE_LABELS, fitted_window_size, setting_label, setting_value, ) from ..viewmodels.downloader import ( DOWNLOAD_SPEED_CUSTOM_LABEL, DOWNLOAD_SPEED_PRESETS_MBPS, download_speed_setting_label, download_speed_setting_value, ) RUNTIME_LABELS: dict[str, str] = { "Automatic (recommended)": "auto", "Proton 7 — Steam Runtime container": "proton7-container", "Proton 7 — Direct": "proton7", "UMU Launcher": "umu", } PROTON_SYNC_LABELS: dict[str, str] = { "Automatic (recommended)": "auto", "Fsync": "fsync", "Esync": "esync", } def open_settings_dialog( root: Any, *, paths: AppPaths, launcher_ui_scale: float, window_icon: Any | None, apply_window_icon: Callable[[Any], None], on_saved: Callable[[Settings], None], existing: Any = None, ) -> Any | None: """Create, or focus, the single settings window owned by a dashboard.""" import tkinter as tk from tkinter import filedialog, ttk active_window: Any = existing if active_window is not None and active_window.winfo_exists(): active_window.deiconify() active_window.lift() active_window.focus_set() return active_window try: current_settings = ensure_settings(paths) except (OSError, ValueError) as settings_error: show_themed_dialog( root, title="Settings", message=f"The launcher could not load its settings:\n\n{settings_error}", kind="error", window_icon=window_icon, ) return None dialog = tk.Toplevel(root) dialog.withdraw() dialog.title("PristonTale EU — Settings") dialog.resizable(False, False) dialog.configure(background="#0c0908") dialog.transient(root) apply_window_icon(dialog) settings_chrome = create_window_chrome( dialog, title="PRISTONTALE EU", subtitle="LAUNCHER SETTINGS", window_icon=window_icon, show_close=True, show_minimize=False, allow_close=True, ) settings_header = tk.Frame( settings_chrome.content, background="#24170f", ) settings_header.pack(fill="x") tk.Frame(settings_header, background="#d98927", width=5).pack(side="left", fill="y") header_copy = tk.Frame(settings_header, background="#24170f") header_copy.pack(side="left", fill="both", expand=True, padx=20, pady=14) tk.Label( header_copy, text="PRISTONTALE EU", background="#24170f", foreground="#e5b66c", font=("Sans", 9, "bold"), ).pack(anchor="w") tk.Label( header_copy, text="LAUNCHER SETTINGS", background="#24170f", foreground="#fff0d2", font=("Georgia", 18, "bold"), ).pack(anchor="w", pady=(1, 0)) tk.Label( header_copy, text=( "Tune launcher accessibility, downloads, the game window, and runtime " "preferences." ), background="#24170f", foreground="#c9baaa", font=("Sans", 9), ).pack(anchor="w", pady=(2, 0)) content = ttk.Frame( settings_chrome.content, style="Root.TFrame", padding=(16, 14, 16, 16), ) content.pack(fill="both", expand=True) content.columnconfigure(0, weight=1, uniform="settings-card") content.columnconfigure(1, weight=1, uniform="settings-card") borderless_value = tk.BooleanVar(value=current_settings.borderless) ui_scale_value = tk.StringVar( value=setting_label(current_settings.ui_scale, UI_SCALE_LABELS) ) suppress_unresponsive_value = tk.BooleanVar( value=current_settings.suppress_unresponsive_warnings ) runtime_value = tk.StringVar( value=setting_label(current_settings.runtime, RUNTIME_LABELS) ) proton_sync_value = tk.StringVar( value=setting_label(current_settings.proton_sync, PROTON_SYNC_LABELS) ) proton_path_value = tk.StringVar(value=current_settings.proton_path or "") game_source_value = tk.StringVar(value=current_settings.game_source or "") download_speed_value = tk.StringVar( value=download_speed_setting_label( current_settings.download_speed_limit_mbps ) ) custom_download_speed_value = tk.StringVar( value=( f"{current_settings.download_speed_limit_mbps:g}" if current_settings.download_speed_limit_mbps is not None else "5" ) ) low_bandwidth_value = tk.BooleanVar( value=current_settings.low_bandwidth_mode ) def add_choice( parent: ttk.LabelFrame, row: int, name: str, label: str, description: str, variable: tk.StringVar, choices: tuple[str, ...], ) -> None: ttk.Label(parent, text=label, style="SettingsText.TLabel").grid( row=row, column=0, sticky="w", ) ttk.Label(parent, text=description, style="SettingsHint.TLabel").grid( row=row, column=1, sticky="e", ) ttk.Combobox( parent, name=name, textvariable=variable, values=choices, state="readonly", style="Settings.TCombobox", cursor="hand2", exportselection=False, takefocus=True, ).grid(row=row + 1, column=0, columnspan=2, sticky="ew", pady=(3, 9)) def add_path( parent: ttk.LabelFrame, row: int, name: str, label: str, description: str, variable: tk.StringVar, browse: Callable[[], None], ) -> None: ttk.Label(parent, text=label, style="SettingsText.TLabel").grid( row=row, column=0, sticky="w", ) ttk.Label(parent, text=description, style="SettingsHint.TLabel").grid( row=row, column=1, sticky="e", ) ttk.Entry( parent, name=name, textvariable=variable, style="Settings.TEntry", exportselection=False, takefocus=True, ).grid( row=row + 1, column=0, columnspan=1, sticky="ew", pady=(3, 8), ) ttk.Button(parent, text="BROWSE", style="Secondary.TButton", command=browse).grid( row=row + 1, column=1, sticky="e", padx=(8, 0), pady=(3, 8), ) def choose_proton_path() -> None: selected = filedialog.askopenfilename( parent=dialog, title="Choose the Proton script", ) if selected: proton_path_value.set(selected) def choose_game_source() -> None: selected = filedialog.askdirectory( parent=dialog, title="Choose an existing PristonTale EU game directory", mustexist=True, ) if selected: game_source_value.set(selected) def close_settings() -> None: if dialog.winfo_exists(): dialog.destroy() settings_chrome.set_close_command(close_settings) def save_current_settings() -> None: try: download_speed_limit = download_speed_setting_value( download_speed_value.get(), custom_download_speed_value.get(), ) updated_settings = update_settings( paths, borderless=borderless_value.get(), ui_scale=setting_value(ui_scale_value.get(), UI_SCALE_LABELS), suppress_unresponsive_warnings=suppress_unresponsive_value.get(), runtime=setting_value(runtime_value.get(), RUNTIME_LABELS), proton_sync=setting_value( proton_sync_value.get(), PROTON_SYNC_LABELS, ), proton_path=proton_path_value.get().strip() or None, game_source=game_source_value.get().strip() or None, download_speed_limit_mbps=download_speed_limit, low_bandwidth_mode=low_bandwidth_value.get(), ) except (OSError, ValueError) as save_error: show_themed_dialog( dialog, title="Settings", message=str(save_error), kind="error", window_icon=window_icon, ) return on_saved(updated_settings) close_settings() display_card = ttk.LabelFrame( content, text="GAME WINDOW", style="SettingsCard.TLabelframe", padding=14, ) display_card.grid(row=0, column=0, sticky="nsew", padx=(0, 6)) display_card.columnconfigure(0, weight=1) display_card.columnconfigure(1, weight=1) ttk.Label( display_card, text=( "Decorated game windows become freely resizable with persistent maximize " "support after startup. Borderless mode removes all host decorations." ), style="SettingsHint.TLabel", wraplength=360, ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 11)) presentation = ttk.Frame(display_card, style="SettingsCard.TFrame") presentation.grid(row=1, column=0, columnspan=2, sticky="ew") ttk.Checkbutton( presentation, text="Remove title bar and borders (Borderless)", variable=borderless_value, style="Settings.TCheckbutton", cursor="hand2", takefocus=True, ).pack(fill="x") ttk.Checkbutton( presentation, text="Suppress false 'not responding' warnings", variable=suppress_unresponsive_value, style="Settings.TCheckbutton", cursor="hand2", takefocus=True, ).pack(fill="x", pady=(7, 0)) add_choice( display_card, 2, "settings_selector_ui_scale", "Launcher size", "Applies after launcher restart", ui_scale_value, tuple(UI_SCALE_LABELS), ) rendering_card = ttk.LabelFrame( content, text="RUNTIME", style="SettingsCard.TLabelframe", padding=14, ) rendering_card.grid(row=0, column=1, sticky="nsew", padx=(6, 0)) rendering_card.columnconfigure(0, weight=1) rendering_card.columnconfigure(1, weight=1) ttk.Label( rendering_card, text="Choose which compatibility runtime launches the proven game setup.", style="SettingsHint.TLabel", wraplength=360, ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 11)) add_choice( rendering_card, 1, "settings_selector_runtime", "Runtime", "Compatibility layer", runtime_value, tuple(RUNTIME_LABELS), ) add_choice( rendering_card, 3, "settings_selector_proton_sync", "Proton synchronization", "Automatic selects the compatible mode", proton_sync_value, tuple(PROTON_SYNC_LABELS), ) downloads_card = ttk.LabelFrame( content, text="DOWNLOADS", style="SettingsCard.TLabelframe", padding=14, ) downloads_card.grid( row=1, column=0, columnspan=2, sticky="ew", pady=(12, 0), ) downloads_card.columnconfigure(0, weight=1) downloads_card.columnconfigure(1, weight=1) downloads_card.columnconfigure(2, weight=1) ttk.Label( downloads_card, text=( "Concurrency adapts to CDN latency, retries, transfer and disk speed, " "CPU load, and the remaining files." ), style="SettingsHint.TLabel", ).grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, 9)) ttk.Checkbutton( downloads_card, text="Low-bandwidth mode (cached news, up to 2 workers and 5 MB/s)", variable=low_bandwidth_value, style="Settings.TCheckbutton", cursor="hand2", takefocus=True, ).grid(row=1, column=0, rowspan=2, sticky="ew", padx=(0, 16)) ttk.Label( downloads_card, text="Speed limit", style="SettingsText.TLabel", ).grid(row=1, column=1, sticky="w") ttk.Label( downloads_card, text="Custom MB/s", style="SettingsText.TLabel", ).grid(row=1, column=2, sticky="w", padx=(8, 0)) download_speed_selector = ttk.Combobox( downloads_card, name="settings_selector_download_speed", textvariable=download_speed_value, values=tuple(DOWNLOAD_SPEED_PRESETS_MBPS) + ( DOWNLOAD_SPEED_CUSTOM_LABEL, ), state="readonly", style="Settings.TCombobox", cursor="hand2", exportselection=False, takefocus=True, ) download_speed_selector.grid( row=2, column=1, sticky="ew", pady=(3, 0), ) custom_download_speed = ttk.Entry( downloads_card, name="settings_download_speed", textvariable=custom_download_speed_value, style="Settings.TEntry", width=12, justify="right", exportselection=False, takefocus=True, ) custom_download_speed.grid( row=2, column=2, sticky="ew", padx=(8, 0), pady=(3, 0), ) def update_custom_download_state(_event: object | None = None) -> None: custom_download_speed.configure( state=( "normal" if download_speed_value.get() == DOWNLOAD_SPEED_CUSTOM_LABEL else "disabled" ) ) download_speed_selector.bind( "<>", update_custom_download_state, ) update_custom_download_state() overrides_card = ttk.LabelFrame( content, text="OPTIONAL OVERRIDES", style="SettingsCard.TLabelframe", padding=14, ) overrides_card.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(12, 0)) overrides_card.columnconfigure(0, weight=1) ttk.Label( overrides_card, text="Leave these blank for automatic runtime and managed-game detection.", style="SettingsHint.TLabel", ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 11)) add_path( overrides_card, 1, "settings_proton_path", "Proton script", "Use a specific Proton executable", proton_path_value, choose_proton_path, ) add_path( overrides_card, 3, "settings_game_source", "Import source", "Seed a new managed installation only", game_source_value, choose_game_source, ) footer = ttk.Frame(content, style="SettingsFooter.TFrame", padding=(12, 9)) footer.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(12, 0)) footer.columnconfigure(0, weight=1) ttk.Label( footer, text="Only the settings shown here affect the next launcher operation.", style="SettingsFooter.TLabel", ).grid(row=0, column=0, sticky="w") buttons_row = ttk.Frame(footer, style="SettingsFooter.TFrame") buttons_row.grid(row=0, column=1, sticky="e") ttk.Button( buttons_row, text="CANCEL", style="Secondary.TButton", command=close_settings, ).pack(side="left") ttk.Button( buttons_row, text="SAVE SETTINGS", style="Play.TButton", command=save_current_settings, ).pack(side="left", padx=(8, 0)) dialog.protocol("WM_DELETE_WINDOW", close_settings) dialog.bind("", lambda _event: close_settings()) dialog.bind("", lambda _event: save_current_settings()) dialog.update_idletasks() settings_width, settings_height = fitted_window_size( 842, 880, scale=launcher_ui_scale, screen_width=dialog.winfo_screenwidth(), screen_height=dialog.winfo_screenheight(), ) dialog.geometry( centered_window_geometry( settings_width, settings_height, parent_x=root.winfo_rootx(), parent_y=root.winfo_rooty(), parent_width=root.winfo_width(), parent_height=root.winfo_height(), ) ) dialog.deiconify() dialog.lift() dialog.grab_set() dialog.focus_set() return dialog