823 lines
22 KiB
Python
823 lines
22 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",
|
|
}
|
|
|
|
SETTINGS_STACKED_LAYOUT_WIDTH = 720
|
|
SETTINGS_STACKED_FOOTER_WIDTH = 640
|
|
SETTINGS_MINIMUM_WIDTH = 620
|
|
SETTINGS_MINIMUM_HEIGHT = 520
|
|
|
|
|
|
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(True, True)
|
|
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))
|
|
|
|
body = ttk.Frame(settings_chrome.content, style="Root.TFrame")
|
|
body.pack(fill="both", expand=True)
|
|
body.columnconfigure(0, weight=1)
|
|
body.rowconfigure(0, weight=1)
|
|
settings_viewport = ttk.Frame(body, name="settings_viewport", style="Root.TFrame")
|
|
settings_viewport.grid(
|
|
row=0,
|
|
column=0,
|
|
sticky="nsew",
|
|
padx=(16, 10),
|
|
pady=(14, 8),
|
|
)
|
|
settings_viewport.columnconfigure(0, weight=1)
|
|
settings_viewport.rowconfigure(0, weight=1)
|
|
settings_canvas = tk.Canvas(
|
|
settings_viewport,
|
|
name="settings_canvas",
|
|
background="#0c0908",
|
|
borderwidth=0,
|
|
highlightthickness=0,
|
|
takefocus=True,
|
|
yscrollincrement=max(1, round(24 * launcher_ui_scale)),
|
|
)
|
|
settings_canvas.grid(row=0, column=0, sticky="nsew")
|
|
settings_scrollbar = ttk.Scrollbar(
|
|
settings_viewport,
|
|
name="settings_scrollbar",
|
|
orient="vertical",
|
|
style="News.Vertical.TScrollbar",
|
|
command=settings_canvas.yview,
|
|
)
|
|
settings_scrollbar.grid(row=0, column=1, sticky="ns", padx=(6, 0))
|
|
settings_canvas.configure(yscrollcommand=settings_scrollbar.set)
|
|
content = ttk.Frame(
|
|
settings_canvas,
|
|
name="settings_content",
|
|
style="Root.TFrame",
|
|
padding=(0, 0, 0, 8),
|
|
)
|
|
content_window = settings_canvas.create_window(
|
|
(0, 0),
|
|
window=content,
|
|
anchor="nw",
|
|
)
|
|
content.columnconfigure(0, weight=1, uniform="settings-card")
|
|
content.columnconfigure(1, weight=1, uniform="settings-card")
|
|
|
|
borderless_value = tk.BooleanVar(value=current_settings.borderless)
|
|
allow_window_resizing_value = tk.BooleanVar(
|
|
value=current_settings.allow_window_resizing
|
|
)
|
|
ui_scale_value = tk.StringVar(
|
|
value=setting_label(current_settings.ui_scale, UI_SCALE_LABELS)
|
|
)
|
|
telemetry_value = tk.BooleanVar(value=current_settings.telemetry_enabled)
|
|
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(),
|
|
allow_window_resizing=allow_window_resizing_value.get(),
|
|
ui_scale=setting_value(ui_scale_value.get(), UI_SCALE_LABELS),
|
|
telemetry_enabled=telemetry_value.get(),
|
|
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)
|
|
display_hint = ttk.Label(
|
|
display_card,
|
|
text=(
|
|
"Gamescope can resize the desktop window while keeping the game's 32-bit "
|
|
"Direct3D surface fixed. The game keeps its aspect ratio as the window "
|
|
"resizes, with black bars when the window has a different shape. Maximize "
|
|
"the window or press Super+F to toggle fullscreen. "
|
|
"If Gamescope is unavailable, the launcher keeps the native game window "
|
|
"at its configured size."
|
|
),
|
|
style="SettingsHint.TLabel",
|
|
wraplength=360,
|
|
)
|
|
display_hint.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="Use a resizable scaled window (Gamescope)",
|
|
variable=allow_window_resizing_value,
|
|
style="Settings.TCheckbutton",
|
|
cursor="hand2",
|
|
takefocus=True,
|
|
).pack(fill="x", pady=(7, 0))
|
|
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)
|
|
rendering_hint = ttk.Label(
|
|
rendering_card,
|
|
text="Choose which compatibility runtime launches the proven game setup.",
|
|
style="SettingsHint.TLabel",
|
|
wraplength=360,
|
|
)
|
|
rendering_hint.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()
|
|
|
|
diagnostics_card = ttk.LabelFrame(
|
|
content,
|
|
text="PRIVACY & DIAGNOSTICS",
|
|
style="SettingsCard.TLabelframe",
|
|
padding=14,
|
|
)
|
|
diagnostics_card.grid(
|
|
row=2,
|
|
column=0,
|
|
columnspan=2,
|
|
sticky="ew",
|
|
pady=(12, 0),
|
|
)
|
|
diagnostics_card.columnconfigure(0, weight=1)
|
|
ttk.Checkbutton(
|
|
diagnostics_card,
|
|
name="settings_telemetry_enabled",
|
|
text="Send anonymous diagnostic and usage information",
|
|
variable=telemetry_value,
|
|
style="Settings.TCheckbutton",
|
|
cursor="hand2",
|
|
takefocus=True,
|
|
).grid(row=0, column=0, sticky="w")
|
|
diagnostics_hint = ttk.Label(
|
|
diagnostics_card,
|
|
text=(
|
|
"Helps improve PristonTale EU Linux compatibility by sending anonymous "
|
|
"launcher errors, Linux and display versions, graphics/runtime compatibility, "
|
|
"and installation or update reliability. Game accounts, personal files, "
|
|
"hostnames, and hardware serial numbers are never collected."
|
|
),
|
|
style="SettingsHint.TLabel",
|
|
wraplength=720,
|
|
)
|
|
diagnostics_hint.grid(row=1, column=0, sticky="w", pady=(7, 0))
|
|
|
|
overrides_card = ttk.LabelFrame(
|
|
content,
|
|
text="OPTIONAL OVERRIDES",
|
|
style="SettingsCard.TLabelframe",
|
|
padding=14,
|
|
)
|
|
overrides_card.grid(row=3, 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(
|
|
body,
|
|
name="settings_footer",
|
|
style="SettingsFooter.TFrame",
|
|
padding=(12, 9),
|
|
)
|
|
footer.grid(row=1, column=0, sticky="ew", padx=16, pady=(0, 16))
|
|
footer.columnconfigure(0, weight=1)
|
|
footer_note = ttk.Label(
|
|
footer,
|
|
text="Only the settings shown here affect the next launcher operation.",
|
|
style="SettingsFooter.TLabel",
|
|
)
|
|
footer_note.grid(row=0, column=0, sticky="w")
|
|
buttons_row = ttk.Frame(
|
|
footer,
|
|
name="settings_buttons",
|
|
style="SettingsFooter.TFrame",
|
|
)
|
|
buttons_row.grid(row=0, column=1, sticky="e")
|
|
ttk.Button(
|
|
buttons_row,
|
|
name="settings_cancel",
|
|
text="CANCEL",
|
|
style="Secondary.TButton",
|
|
command=close_settings,
|
|
).pack(side="left")
|
|
ttk.Button(
|
|
buttons_row,
|
|
name="settings_save",
|
|
text="SAVE SETTINGS",
|
|
style="Play.TButton",
|
|
command=save_current_settings,
|
|
).pack(side="left", padx=(8, 0))
|
|
|
|
settings_layout_mode: tuple[bool, bool] | None = None
|
|
settings_layout_width: int | None = None
|
|
pending_settings_width = 1
|
|
settings_layout_after: str | None = None
|
|
|
|
def arrange_settings(viewport_width: int) -> None:
|
|
"""Reflow cards and footer controls for the available logical width."""
|
|
|
|
nonlocal settings_layout_mode, settings_layout_width
|
|
if settings_layout_width == viewport_width:
|
|
return
|
|
settings_layout_width = viewport_width
|
|
logical_width = viewport_width / max(launcher_ui_scale, 0.01)
|
|
stacked = logical_width < SETTINGS_STACKED_LAYOUT_WIDTH
|
|
footer_stacked = logical_width < SETTINGS_STACKED_FOOTER_WIDTH
|
|
card_width = (
|
|
viewport_width
|
|
if stacked
|
|
else (viewport_width - round(12 * launcher_ui_scale)) // 2
|
|
)
|
|
card_wraplength = max(140, card_width - round(42 * launcher_ui_scale))
|
|
display_hint.configure(wraplength=card_wraplength)
|
|
rendering_hint.configure(wraplength=card_wraplength)
|
|
diagnostics_hint.configure(
|
|
wraplength=max(140, viewport_width - round(42 * launcher_ui_scale))
|
|
)
|
|
footer_note.configure(
|
|
wraplength=(
|
|
max(140, viewport_width - round(42 * launcher_ui_scale))
|
|
if footer_stacked
|
|
else max(140, viewport_width - round(300 * launcher_ui_scale))
|
|
)
|
|
)
|
|
|
|
mode = (stacked, footer_stacked)
|
|
if settings_layout_mode == mode:
|
|
return
|
|
settings_layout_mode = mode
|
|
if stacked:
|
|
display_card.grid_configure(
|
|
row=0,
|
|
column=0,
|
|
columnspan=2,
|
|
sticky="ew",
|
|
padx=0,
|
|
pady=0,
|
|
)
|
|
rendering_card.grid_configure(
|
|
row=1,
|
|
column=0,
|
|
columnspan=2,
|
|
sticky="ew",
|
|
padx=0,
|
|
pady=(12, 0),
|
|
)
|
|
downloads_card.grid_configure(row=2, pady=(12, 0))
|
|
diagnostics_card.grid_configure(row=3, pady=(12, 0))
|
|
overrides_card.grid_configure(row=4, pady=(12, 0))
|
|
else:
|
|
display_card.grid_configure(
|
|
row=0,
|
|
column=0,
|
|
columnspan=1,
|
|
sticky="nsew",
|
|
padx=(0, 6),
|
|
pady=0,
|
|
)
|
|
rendering_card.grid_configure(
|
|
row=0,
|
|
column=1,
|
|
columnspan=1,
|
|
sticky="nsew",
|
|
padx=(6, 0),
|
|
pady=0,
|
|
)
|
|
downloads_card.grid_configure(row=1, pady=(12, 0))
|
|
diagnostics_card.grid_configure(row=2, pady=(12, 0))
|
|
overrides_card.grid_configure(row=3, pady=(12, 0))
|
|
|
|
if footer_stacked:
|
|
footer_note.grid_configure(row=0, column=0, columnspan=2)
|
|
buttons_row.grid_configure(
|
|
row=1,
|
|
column=0,
|
|
columnspan=2,
|
|
pady=(8, 0),
|
|
)
|
|
else:
|
|
footer_note.grid_configure(row=0, column=0, columnspan=1)
|
|
buttons_row.grid_configure(
|
|
row=0,
|
|
column=1,
|
|
columnspan=1,
|
|
pady=0,
|
|
)
|
|
|
|
def refresh_scroll_region(_event: object | None = None) -> None:
|
|
bounds = settings_canvas.bbox("all")
|
|
if bounds is not None:
|
|
settings_canvas.configure(scrollregion=bounds)
|
|
|
|
def apply_pending_settings_layout() -> None:
|
|
nonlocal settings_layout_after
|
|
settings_layout_after = None
|
|
arrange_settings(pending_settings_width)
|
|
|
|
def resize_settings_content(event: Any) -> None:
|
|
nonlocal pending_settings_width, settings_layout_after
|
|
pending_settings_width = max(1, int(event.width))
|
|
settings_canvas.itemconfigure(content_window, width=pending_settings_width)
|
|
if settings_layout_after is None:
|
|
settings_layout_after = dialog.after_idle(apply_pending_settings_layout)
|
|
|
|
def scroll_settings(event: Any) -> str | None:
|
|
canvas_left = settings_canvas.winfo_rootx()
|
|
canvas_top = settings_canvas.winfo_rooty()
|
|
pointer_x = dialog.winfo_pointerx()
|
|
pointer_y = dialog.winfo_pointery()
|
|
if not (
|
|
canvas_left <= pointer_x < canvas_left + settings_canvas.winfo_width()
|
|
and canvas_top <= pointer_y < canvas_top + settings_canvas.winfo_height()
|
|
):
|
|
return None
|
|
if getattr(event.widget, "winfo_class", lambda: "")() == "TCombobox":
|
|
return None
|
|
first, last = settings_canvas.yview()
|
|
if last - first >= 0.999:
|
|
return None
|
|
if getattr(event, "num", None) == 4:
|
|
units = -3
|
|
elif getattr(event, "num", None) == 5:
|
|
units = 3
|
|
else:
|
|
delta = int(getattr(event, "delta", 0))
|
|
if delta == 0:
|
|
return None
|
|
steps = max(1, min(4, abs(delta) // 120 or 1))
|
|
units = -steps if delta > 0 else steps
|
|
settings_canvas.yview_scroll(units, "units")
|
|
return "break"
|
|
|
|
content.bind("<Configure>", refresh_scroll_region)
|
|
settings_canvas.bind("<Configure>", resize_settings_content)
|
|
dialog.bind("<MouseWheel>", scroll_settings, add="+")
|
|
dialog.bind("<Button-4>", scroll_settings, add="+")
|
|
dialog.bind("<Button-5>", scroll_settings, add="+")
|
|
settings_canvas.bind("<Home>", lambda _event: settings_canvas.yview_moveto(0.0))
|
|
settings_canvas.bind("<End>", lambda _event: settings_canvas.yview_moveto(1.0))
|
|
settings_canvas.bind(
|
|
"<Prior>",
|
|
lambda _event: settings_canvas.yview_scroll(-1, "pages"),
|
|
)
|
|
settings_canvas.bind(
|
|
"<Next>",
|
|
lambda _event: settings_canvas.yview_scroll(1, "pages"),
|
|
)
|
|
|
|
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(),
|
|
)
|
|
minimum_width, minimum_height = fitted_window_size(
|
|
SETTINGS_MINIMUM_WIDTH,
|
|
SETTINGS_MINIMUM_HEIGHT,
|
|
scale=launcher_ui_scale,
|
|
screen_width=dialog.winfo_screenwidth(),
|
|
screen_height=dialog.winfo_screenheight(),
|
|
)
|
|
dialog.minsize(minimum_width, minimum_height)
|
|
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
|