Reworked UI for easier workflow.
This commit is contained in:
514
ptlinux/ui/dialogs/settings_dialog.py
Normal file
514
ptlinux/ui/dialogs/settings_dialog.py
Normal file
@@ -0,0 +1,514 @@
|
||||
"""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,
|
||||
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,
|
||||
textvariable=variable,
|
||||
values=choices,
|
||||
state="readonly",
|
||||
style="Settings.TCombobox",
|
||||
).grid(row=row + 1, column=0, columnspan=2, sticky="ew", pady=(3, 9))
|
||||
|
||||
def add_path(
|
||||
parent: ttk.LabelFrame,
|
||||
row: int,
|
||||
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, textvariable=variable, style="Settings.TEntry").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="w")
|
||||
ttk.Checkbutton(
|
||||
presentation,
|
||||
text="Remove title bar and borders (Borderless)",
|
||||
variable=borderless_value,
|
||||
style="Settings.TCheckbutton",
|
||||
).pack(anchor="w")
|
||||
ttk.Checkbutton(
|
||||
presentation,
|
||||
text="Suppress false 'not responding' warnings",
|
||||
variable=suppress_unresponsive_value,
|
||||
style="Settings.TCheckbutton",
|
||||
).pack(anchor="w", pady=(7, 0))
|
||||
add_choice(
|
||||
display_card,
|
||||
2,
|
||||
"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,
|
||||
"Runtime",
|
||||
"Compatibility layer",
|
||||
runtime_value,
|
||||
tuple(RUNTIME_LABELS),
|
||||
)
|
||||
add_choice(
|
||||
rendering_card,
|
||||
3,
|
||||
"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",
|
||||
).grid(row=1, column=0, sticky="w", 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,
|
||||
textvariable=download_speed_value,
|
||||
values=tuple(DOWNLOAD_SPEED_PRESETS_MBPS) + (
|
||||
DOWNLOAD_SPEED_CUSTOM_LABEL,
|
||||
),
|
||||
state="readonly",
|
||||
style="Settings.TCombobox",
|
||||
)
|
||||
download_speed_selector.grid(
|
||||
row=2,
|
||||
column=1,
|
||||
sticky="ew",
|
||||
pady=(3, 0),
|
||||
)
|
||||
custom_download_speed = ttk.Entry(
|
||||
downloads_card,
|
||||
textvariable=custom_download_speed_value,
|
||||
style="Settings.TEntry",
|
||||
width=12,
|
||||
)
|
||||
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,
|
||||
"Proton script",
|
||||
"Use a specific Proton executable",
|
||||
proton_path_value,
|
||||
choose_proton_path,
|
||||
)
|
||||
add_path(
|
||||
overrides_card,
|
||||
3,
|
||||
"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
|
||||
Reference in New Issue
Block a user