Files
pristontale-eu-linux/ptlinux/ui/dialogs/settings_dialog.py

875 lines
23 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",
}
UPDATE_CHANNEL_LABELS: dict[str, str] = {
"Stable — Recommended for normal use": "stable",
"Prerelease — Beta and release candidate builds": "prerelease",
}
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
)
update_channel_value = tk.StringVar(
value=setting_label(current_settings.update_channel, UPDATE_CHANNEL_LABELS)
)
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(),
update_channel=setting_value(
update_channel_value.get(),
UPDATE_CHANNEL_LABELS,
),
)
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()
updates_card = ttk.LabelFrame(
content,
text="LAUNCHER UPDATES",
style="SettingsCard.TLabelframe",
padding=14,
)
updates_card.grid(
row=2,
column=0,
columnspan=2,
sticky="ew",
pady=(12, 0),
)
updates_card.columnconfigure(0, weight=1)
updates_card.columnconfigure(1, weight=1)
add_choice(
updates_card,
0,
"settings_selector_update_channel",
"Update channel",
"Existing installations default to Stable",
update_channel_value,
tuple(UPDATE_CHANNEL_LABELS),
)
updates_hint = ttk.Label(
updates_card,
text=(
"Stable receives production releases. Prerelease also receives beta and "
"release candidate builds. Returning to Stable never downgrades the "
"installed launcher automatically."
),
style="SettingsHint.TLabel",
wraplength=720,
)
updates_hint.grid(row=2, column=0, columnspan=2, sticky="w")
diagnostics_card = ttk.LabelFrame(
content,
text="PRIVACY & DIAGNOSTICS",
style="SettingsCard.TLabelframe",
padding=14,
)
diagnostics_card.grid(
row=3,
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=4, 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)
updates_hint.configure(
wraplength=max(140, viewport_width - round(42 * launcher_ui_scale))
)
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))
updates_card.grid_configure(row=3, pady=(12, 0))
diagnostics_card.grid_configure(row=4, pady=(12, 0))
overrides_card.grid_configure(row=5, 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))
updates_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))
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