From 87dee93b86cb04e3bb07dd596f0443443db52cec Mon Sep 17 00:00:00 2001 From: m4ko Date: Mon, 31 Aug 2026 22:03:47 +0200 Subject: [PATCH] Reworked the modal ui's for launcher, settings, updates and game pre-loader. --- ptlinux/ui.py | 251 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 189 insertions(+), 62 deletions(-) diff --git a/ptlinux/ui.py b/ptlinux/ui.py index 0b00e3a..63f159d 100644 --- a/ptlinux/ui.py +++ b/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: