Reworked the modal ui's for launcher, settings, updates and game pre-loader.
This commit is contained in:
251
ptlinux/ui.py
251
ptlinux/ui.py
@@ -14,7 +14,10 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
from typing import Callable, Mapping
|
||||
from typing import TYPE_CHECKING, Callable, Mapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tkinter import Frame, PhotoImage, Toplevel
|
||||
|
||||
from . import __version__
|
||||
from .changelog import (
|
||||
@@ -75,10 +78,17 @@ from .server_status import (
|
||||
ServerStatusError,
|
||||
fetch_server_status,
|
||||
)
|
||||
from .tk_chrome import (
|
||||
centered_window_geometry,
|
||||
create_dashboard_chrome,
|
||||
create_window_chrome,
|
||||
load_square_window_icon,
|
||||
show_themed_dialog,
|
||||
)
|
||||
|
||||
|
||||
MAX_NEWS_CARDS = 3
|
||||
WINDOW_SIZE = "1000x750"
|
||||
WINDOW_SIZE = "1002x784"
|
||||
NEWS_CARD_HEIGHT = 242
|
||||
NEWS_IMAGE_WIDTH = 292
|
||||
NEWS_IMAGE_HEIGHT = 118
|
||||
@@ -517,7 +527,7 @@ def preloader_status_text(
|
||||
return f"Step {step} of {total} · {value}"
|
||||
|
||||
|
||||
def freeze_preloader_geometry(frame: tk.Misc) -> tuple[int, int]:
|
||||
def freeze_preloader_geometry(frame: Frame) -> tuple[int, int]:
|
||||
"""Lock a measured preloader frame so later status text cannot reflow it."""
|
||||
|
||||
frame.update_idletasks()
|
||||
@@ -654,7 +664,7 @@ class TransferRateEstimator:
|
||||
def run_dashboard(paths: AppPaths) -> None:
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
from tkinter import filedialog, ttk
|
||||
except ImportError as exc:
|
||||
raise DashboardError(
|
||||
"Python Tk is missing. Install your distribution's python3-tk package "
|
||||
@@ -665,29 +675,43 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
root = tk.Tk(className=APPLICATION_WM_CLASS)
|
||||
root.title(f"{APPLICATION_NAME} — Linux Launcher")
|
||||
root.iconname(APPLICATION_NAME)
|
||||
window_icon = None
|
||||
window_icon: PhotoImage | None = None
|
||||
window_icon_path = find_application_icon()
|
||||
if window_icon_path is not None:
|
||||
try:
|
||||
window_icon = tk.PhotoImage(master=root, file=str(window_icon_path))
|
||||
root.iconphoto(True, window_icon)
|
||||
loaded_window_icon = load_square_window_icon(root, window_icon_path)
|
||||
root.iconphoto(True, loaded_window_icon)
|
||||
window_icon = loaded_window_icon
|
||||
except (OSError, tk.TclError) as icon_error:
|
||||
print(
|
||||
f"Warning: could not load the launcher icon: {icon_error}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
def apply_window_icon(window: Toplevel) -> None:
|
||||
icon = window_icon
|
||||
if icon is not None:
|
||||
window.iconphoto(False, icon)
|
||||
|
||||
root.geometry(WINDOW_SIZE)
|
||||
root.resizable(False, False)
|
||||
root.configure(background="#0c0908")
|
||||
root.withdraw()
|
||||
dashboard_chrome = create_dashboard_chrome(
|
||||
root,
|
||||
title="PRISTONTALE EU",
|
||||
subtitle="NATIVE LINUX LAUNCHER",
|
||||
)
|
||||
dashboard_content = dashboard_chrome.content
|
||||
try:
|
||||
startup_settings = ensure_settings(paths)
|
||||
except (OSError, ValueError) as startup_error:
|
||||
root.deiconify()
|
||||
messagebox.showerror(
|
||||
"Launcher settings",
|
||||
f"The launcher could not load its settings:\n\n{startup_error}",
|
||||
parent=root,
|
||||
show_themed_dialog(
|
||||
root,
|
||||
title="Launcher settings",
|
||||
message=f"The launcher could not load its settings:\n\n{startup_error}",
|
||||
kind="error",
|
||||
)
|
||||
root.destroy()
|
||||
return
|
||||
@@ -1091,7 +1115,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
startup_started_at = time.monotonic()
|
||||
startup_status = tk.StringVar(value="Loading dashboard contents…")
|
||||
startup_overlay = tk.Frame(
|
||||
root,
|
||||
dashboard_content,
|
||||
background="#0c0908",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
@@ -1289,7 +1313,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
startup_timeout_after = None
|
||||
begin_dashboard_transition(timed_out=True)
|
||||
|
||||
frame = ttk.Frame(root, style="Root.TFrame", padding=12)
|
||||
frame = ttk.Frame(dashboard_content, style="Root.TFrame", padding=12)
|
||||
frame.pack(fill="both", expand=True)
|
||||
|
||||
header = ttk.Frame(frame, style="Root.TFrame")
|
||||
@@ -1535,6 +1559,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
activity_window.geometry("800x400")
|
||||
activity_window.resizable(False, False)
|
||||
activity_window.configure(background="#0c0908")
|
||||
apply_window_icon(activity_window)
|
||||
log = tk.Text(
|
||||
activity_window,
|
||||
background="#100d0c",
|
||||
@@ -1577,6 +1602,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
preloader: tk.Toplevel | None = None
|
||||
preloader_status: tk.StringVar | None = None
|
||||
preloader_progress: ttk.Progressbar | None = None
|
||||
preloader_image_references: list[tk.PhotoImage] = []
|
||||
settings_window: tk.Toplevel | None = None
|
||||
dashboard_debug_window: tk.Toplevel | None = None
|
||||
dashboard_debug_output: tk.Text | None = None
|
||||
@@ -1753,37 +1779,64 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
if preloader is not None and preloader.winfo_exists():
|
||||
return
|
||||
window = tk.Toplevel(root)
|
||||
window.withdraw()
|
||||
preloader = window
|
||||
window.title("Launching PristonTale EU")
|
||||
window.configure(background="#0c0908")
|
||||
window.resizable(False, False)
|
||||
window.protocol("WM_DELETE_WINDOW", lambda: None)
|
||||
window.transient(root)
|
||||
window.attributes("-topmost", True)
|
||||
apply_window_icon(window)
|
||||
loader_chrome = create_window_chrome(
|
||||
window,
|
||||
title="",
|
||||
subtitle="",
|
||||
show_title_bar=False,
|
||||
show_close=False,
|
||||
show_minimize=False,
|
||||
allow_close=False,
|
||||
)
|
||||
status_variable = tk.StringVar(value="Starting launcher process…")
|
||||
preloader_status = status_variable
|
||||
content = ttk.Frame(window, style="Root.TFrame", padding=26)
|
||||
content.pack(fill="both", expand=True)
|
||||
ttk.Label(content, text="PRISTONTALE EU", style="Brand.TLabel").pack(anchor="w")
|
||||
ttk.Label(content, text="LAUNCHING THE CLIENT", style="Tagline.TLabel").pack(
|
||||
anchor="w",
|
||||
pady=(2, 18),
|
||||
content = ttk.Frame(
|
||||
loader_chrome.content,
|
||||
style="Root.TFrame",
|
||||
padding=(34, 26),
|
||||
)
|
||||
content.pack(fill="both", expand=True)
|
||||
logo_source = window_icon
|
||||
if logo_source is not None:
|
||||
factor = max(
|
||||
1,
|
||||
math.ceil(logo_source.width() / 88),
|
||||
math.ceil(logo_source.height() / 88),
|
||||
)
|
||||
modal_logo = logo_source.subsample(factor, factor)
|
||||
preloader_image_references.clear()
|
||||
preloader_image_references.append(modal_logo)
|
||||
tk.Label(
|
||||
content,
|
||||
image=modal_logo,
|
||||
background="#0c0908",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
).pack(anchor="center", pady=(0, 16))
|
||||
ttk.Label(
|
||||
content,
|
||||
text="Please wait while the game prepares its window.",
|
||||
text="Preparing the game window…",
|
||||
style="PreloaderStatus.TLabel",
|
||||
).pack(anchor="w")
|
||||
anchor="center",
|
||||
).pack(fill="x")
|
||||
status_label = ttk.Label(
|
||||
content,
|
||||
textvariable=status_variable,
|
||||
style="PreloaderMuted.TLabel",
|
||||
width=PRELOADER_STATUS_CHARACTERS,
|
||||
anchor="w",
|
||||
anchor="center",
|
||||
)
|
||||
status_label.pack(
|
||||
fill="x",
|
||||
pady=(4, 10),
|
||||
pady=(5, 11),
|
||||
)
|
||||
progress_widget = ttk.Progressbar(
|
||||
content,
|
||||
@@ -1795,12 +1848,23 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
preloader_progress = progress_widget
|
||||
progress_widget.pack(fill="x")
|
||||
freeze_preloader_geometry(content)
|
||||
root.update_idletasks()
|
||||
window.update_idletasks()
|
||||
width = window.winfo_reqwidth()
|
||||
height = window.winfo_reqheight()
|
||||
x = (window.winfo_screenwidth() - width) // 2
|
||||
y = (window.winfo_screenheight() - height) // 2
|
||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
window.geometry(
|
||||
centered_window_geometry(
|
||||
width,
|
||||
height,
|
||||
parent_x=root.winfo_rootx(),
|
||||
parent_y=root.winfo_rooty(),
|
||||
parent_width=root.winfo_width(),
|
||||
parent_height=root.winfo_height(),
|
||||
)
|
||||
)
|
||||
window.deiconify()
|
||||
window.lift()
|
||||
window.grab_set()
|
||||
window.focus_force()
|
||||
|
||||
def set_preloader_status(value: str, step: int | None = None, total: int | None = None) -> None:
|
||||
@@ -1812,10 +1876,15 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
def close_game_preloader() -> None:
|
||||
nonlocal preloader, preloader_status, preloader_progress
|
||||
if preloader is not None and preloader.winfo_exists():
|
||||
try:
|
||||
preloader.grab_release()
|
||||
except tk.TclError:
|
||||
pass
|
||||
preloader.destroy()
|
||||
preloader = None
|
||||
preloader_status = None
|
||||
preloader_progress = None
|
||||
preloader_image_references.clear()
|
||||
|
||||
def run_operation(
|
||||
label: str,
|
||||
@@ -2720,6 +2789,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
window.geometry("900x640")
|
||||
window.minsize(700, 440)
|
||||
window.configure(background="#0c0908")
|
||||
apply_window_icon(window)
|
||||
debug_header = tk.Frame(window, background="#24170f", padx=16, pady=12)
|
||||
debug_header.pack(fill="x")
|
||||
tk.Label(
|
||||
@@ -3103,10 +3173,14 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
start_new_session=True,
|
||||
)
|
||||
except OSError as restart_error:
|
||||
messagebox.showerror(
|
||||
"Launcher update",
|
||||
f"The update was installed, but the launcher could not restart: {restart_error}",
|
||||
parent=root,
|
||||
show_themed_dialog(
|
||||
root,
|
||||
title="Launcher update",
|
||||
message=(
|
||||
"The update was installed, but the launcher could not "
|
||||
f"restart: {restart_error}"
|
||||
),
|
||||
kind="error",
|
||||
)
|
||||
else:
|
||||
root.destroy()
|
||||
@@ -3342,30 +3416,40 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
return
|
||||
if not check.available:
|
||||
if required_reason is None:
|
||||
messagebox.showinfo(
|
||||
"Launcher update",
|
||||
check.reason,
|
||||
parent=root,
|
||||
show_themed_dialog(
|
||||
root,
|
||||
title="Launcher is up to date",
|
||||
message=check.reason,
|
||||
kind="success",
|
||||
)
|
||||
else:
|
||||
messagebox.showwarning(
|
||||
"Compatibility update required",
|
||||
"The installed game needs a launcher compatibility update:\n\n"
|
||||
f"{required_reason}\n\n{check.reason}",
|
||||
parent=root,
|
||||
show_themed_dialog(
|
||||
root,
|
||||
title="Compatibility update required",
|
||||
message=(
|
||||
"The installed game needs a launcher compatibility update:\n\n"
|
||||
f"{required_reason}\n\n{check.reason}"
|
||||
),
|
||||
kind="warning",
|
||||
)
|
||||
return
|
||||
if not check.supported:
|
||||
messagebox.showwarning(
|
||||
"Manual launcher update required",
|
||||
check.reason,
|
||||
parent=root,
|
||||
show_themed_dialog(
|
||||
root,
|
||||
title="Manual launcher update required",
|
||||
message=check.reason,
|
||||
kind="warning",
|
||||
)
|
||||
return
|
||||
if not messagebox.askyesno(
|
||||
"Update launcher",
|
||||
f"Install signed launcher version {check.latest_version} and restart now?",
|
||||
parent=root,
|
||||
if not show_themed_dialog(
|
||||
root,
|
||||
title="Update launcher",
|
||||
message=(
|
||||
f"Install signed launcher version {check.latest_version} and restart now?"
|
||||
),
|
||||
kind="question",
|
||||
confirm=True,
|
||||
accept_text="INSTALL & RESTART",
|
||||
):
|
||||
return
|
||||
run_operation(
|
||||
@@ -3385,11 +3469,14 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
official_snapshot,
|
||||
)
|
||||
except OSError as cache_error:
|
||||
messagebox.showerror(
|
||||
"Realm selection",
|
||||
"The launcher could not save the official realm selection:\n\n"
|
||||
f"{cache_error}",
|
||||
parent=root,
|
||||
show_themed_dialog(
|
||||
root,
|
||||
title="Realm selection",
|
||||
message=(
|
||||
"The launcher could not save the official realm selection:\n\n"
|
||||
f"{cache_error}"
|
||||
),
|
||||
kind="error",
|
||||
)
|
||||
return
|
||||
launch_settings: Settings | None = None
|
||||
@@ -3469,21 +3556,36 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
try:
|
||||
current_settings = ensure_settings(paths)
|
||||
except (OSError, ValueError) as error:
|
||||
messagebox.showerror(
|
||||
"Settings",
|
||||
f"The launcher could not load its settings:\n\n{error}",
|
||||
parent=root,
|
||||
show_themed_dialog(
|
||||
root,
|
||||
title="Settings",
|
||||
message=f"The launcher could not load its settings:\n\n{error}",
|
||||
kind="error",
|
||||
)
|
||||
return
|
||||
dialog = tk.Toplevel(root)
|
||||
dialog.withdraw()
|
||||
settings_window = dialog
|
||||
dialog.title("PristonTale EU — Settings")
|
||||
dialog.geometry("840x680")
|
||||
dialog.geometry("842x714")
|
||||
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",
|
||||
show_close=True,
|
||||
show_minimize=False,
|
||||
allow_close=True,
|
||||
)
|
||||
|
||||
settings_header = tk.Frame(dialog, background="#24170f", height=118)
|
||||
settings_header = tk.Frame(
|
||||
settings_chrome.content,
|
||||
background="#24170f",
|
||||
height=118,
|
||||
)
|
||||
settings_header.pack(fill="x")
|
||||
settings_header.pack_propagate(False)
|
||||
tk.Frame(settings_header, background="#d98927", width=5).pack(side="left", fill="y")
|
||||
@@ -3511,7 +3613,11 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
font=("Sans", 9),
|
||||
).pack(anchor="w", pady=(2, 0))
|
||||
|
||||
content = ttk.Frame(dialog, style="Root.TFrame", padding=(16, 14, 16, 16))
|
||||
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")
|
||||
@@ -3607,6 +3713,8 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
settings_window.destroy()
|
||||
settings_window = None
|
||||
|
||||
settings_chrome.set_close_command(close_settings)
|
||||
|
||||
def save_current_settings() -> None:
|
||||
try:
|
||||
updated_settings = update_settings(
|
||||
@@ -3619,7 +3727,12 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
game_source=game_source_value.get().strip() or None,
|
||||
)
|
||||
except (OSError, ValueError) as error:
|
||||
messagebox.showerror("Settings", str(error), parent=dialog)
|
||||
show_themed_dialog(
|
||||
dialog,
|
||||
title="Settings",
|
||||
message=str(error),
|
||||
kind="error",
|
||||
)
|
||||
return
|
||||
append_log(
|
||||
f"Saved launcher settings to {paths.settings_file}: "
|
||||
@@ -3752,7 +3865,20 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
).pack(side="left", padx=(8, 0))
|
||||
|
||||
dialog.protocol("WM_DELETE_WINDOW", close_settings)
|
||||
dialog.update_idletasks()
|
||||
dialog.geometry(
|
||||
centered_window_geometry(
|
||||
842,
|
||||
714,
|
||||
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()
|
||||
|
||||
settings_button.configure(command=open_settings)
|
||||
@@ -3779,10 +3905,11 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
# and any active launcher operation have finished.
|
||||
if operation_active or game_clients.active_count:
|
||||
set_dashboard_debug(False)
|
||||
root.iconify()
|
||||
dashboard_chrome.minimize()
|
||||
return
|
||||
root.destroy()
|
||||
|
||||
dashboard_chrome.set_close_command(close_dashboard)
|
||||
root.protocol("WM_DELETE_WINDOW", close_dashboard)
|
||||
|
||||
def update_news_viewport(_event: object | None = None) -> None:
|
||||
|
||||
Reference in New Issue
Block a user