diff --git a/ptlinux/ui/dialogs/settings_dialog.py b/ptlinux/ui/dialogs/settings_dialog.py index 4e08cb9..99ae263 100644 --- a/ptlinux/ui/dialogs/settings_dialog.py +++ b/ptlinux/ui/dialogs/settings_dialog.py @@ -37,6 +37,11 @@ PROTON_SYNC_LABELS: dict[str, str] = { "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, @@ -74,7 +79,7 @@ def open_settings_dialog( dialog = tk.Toplevel(root) dialog.withdraw() dialog.title("PristonTale EU — Settings") - dialog.resizable(False, False) + dialog.resizable(True, True) dialog.configure(background="#0c0908") dialog.transient(root) apply_window_icon(dialog) @@ -121,12 +126,50 @@ def open_settings_dialog( font=("Sans", 9), ).pack(anchor="w", pady=(2, 0)) - content = ttk.Frame( - settings_chrome.content, - style="Root.TFrame", - padding=(16, 14, 16, 16), + 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.pack(fill="both", expand=True) content.columnconfigure(0, weight=1, uniform="settings-card") content.columnconfigure(1, weight=1, uniform="settings-card") @@ -302,7 +345,7 @@ def open_settings_dialog( 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_hint = ttk.Label( display_card, text=( "Gamescope can resize the desktop window while keeping the game's 32-bit " @@ -314,7 +357,8 @@ def open_settings_dialog( ), style="SettingsHint.TLabel", wraplength=360, - ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 11)) + ) + 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( @@ -360,12 +404,13 @@ def open_settings_dialog( 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_hint = 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)) + ) + rendering_hint.grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 11)) add_choice( rendering_card, 1, @@ -511,29 +556,193 @@ def open_settings_dialog( 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 = 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) - ttk.Label( + footer_note = 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") + ) + 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) + 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)) + overrides_card.grid_configure(row=3, 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)) + overrides_card.grid_configure(row=2, 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("", refresh_scroll_region) + settings_canvas.bind("", resize_settings_content) + dialog.bind("", scroll_settings, add="+") + dialog.bind("", scroll_settings, add="+") + dialog.bind("", scroll_settings, add="+") + settings_canvas.bind("", lambda _event: settings_canvas.yview_moveto(0.0)) + settings_canvas.bind("", lambda _event: settings_canvas.yview_moveto(1.0)) + settings_canvas.bind( + "", + lambda _event: settings_canvas.yview_scroll(-1, "pages"), + ) + settings_canvas.bind( + "", + lambda _event: settings_canvas.yview_scroll(1, "pages"), + ) + dialog.protocol("WM_DELETE_WINDOW", close_settings) dialog.bind("", lambda _event: close_settings()) dialog.bind("", lambda _event: save_current_settings()) @@ -545,6 +754,14 @@ def open_settings_dialog( 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,