Files
pristontale-eu-linux/ptlinux/ui/dialogs/settings_dialog.py
2026-09-03 21:52:06 +02:00

547 lines
14 KiB
Python

"""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=(
"The launcher keeps the game window's title bar by default. Enable "
"Borderless only when you want to remove its 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(
"<<ComboboxSelected>>",
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("<Escape>", lambda _event: close_settings())
dialog.bind("<Control-s>", 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