diff --git a/src/TTS_ka/gui.py b/src/TTS_ka/gui.py index ebf1949..be9b644 100644 --- a/src/TTS_ka/gui.py +++ b/src/TTS_ka/gui.py @@ -419,14 +419,16 @@ def _build_setup_tab(self, frm: Any) -> None: ).grid(row=1, column=0, sticky=tk.W, pady=(2, 10)) # --- Step 1: verify setup ------------------------------------------- - ttk.Label(frm, text="1. Verify your setup", style="Heading.TLabel").grid(row=2, column=0, sticky=tk.W) - self._setup_report = _scrolled_text(frm, height=9, wrap=tk.WORD) - self._setup_report.grid(row=3, column=0, sticky=(tk.E, tk.W), pady=(4, 0)) + step1 = ttk.Frame(frm) + step1.grid(row=2, column=0, sticky=tk.EW) + ttk.Label(step1, text="1. Verify your setup", style="Heading.TLabel").pack(side=tk.LEFT) + ttk.Button(step1, text="Re-check", command=self._on_setup_recheck).pack(side=tk.RIGHT) + # The report is the row that gives way, so steps 2 and 3 stay visible at the minimum size. + self._setup_report = _scrolled_text(frm, height=6, wrap=tk.WORD) + self._setup_report.grid(row=3, column=0, sticky=tk.NSEW, pady=(4, 0)) + frm.rowconfigure(3, weight=1) self._setup_report.insert("1.0", "Checking dependencies…") self._setup_report.configure(state=tk.DISABLED) - ttk.Button(frm, text="Re-check", command=self._on_setup_recheck).grid( - row=4, column=0, sticky=tk.W, pady=(4, 0) - ) ttk.Separator(frm, orient=tk.HORIZONTAL).grid( row=5, column=0, sticky=(tk.E, tk.W), pady=12 @@ -449,8 +451,11 @@ def _build_setup_tab(self, frm: Any) -> None: ) self._setup_preview_btn.pack(side=tk.LEFT, padx=(8, 0)) self.setup_status = tk.StringVar(value="") - ttk.Label(frm, textvariable=self.setup_status, wraplength=520, justify=tk.LEFT).grid( - row=8, column=0, sticky=tk.W, pady=(4, 0) + status_lbl = ttk.Label(frm, textvariable=self.setup_status, wraplength=520, justify=tk.LEFT) + status_lbl.grid(row=8, column=0, sticky=tk.W, pady=(4, 0)) + status_lbl.grid_remove() # an empty line would push step 3 down + self.setup_status.trace_add( + "write", lambda *_: status_lbl.grid() if self.setup_status.get() else status_lbl.grid_remove() ) ttk.Separator(frm, orient=tk.HORIZONTAL).grid( @@ -668,13 +673,14 @@ def _build_config_tab(self, frm: Any) -> None: ttk.Checkbutton( frm, - text="VLC RC playlist mode on Windows (vlc_rc; uncheck sets TTS_KA_VLC_RC=0)", + text="Windows: play all chunks in one VLC window (vlc_rc)", variable=self.cfg_vlc_rc, - ).grid(row=7, column=0, columnspan=2, sticky=tk.W) + ).grid(row=7, column=0, columnspan=3, sticky=tk.W) help_txt = ( "Saved keys match ~/.tts_config.json: lang (Speak tab), output, chunk_seconds, " - "parallel, no_play, stream, no_gui, no_turbo, vlc_rc." + "parallel, no_play, stream, no_gui, no_turbo, vlc_rc. " + "Unticking the VLC option sets TTS_KA_VLC_RC=0 (one VLC per chunk)." ) ttk.Label(frm, text=help_txt, wraplength=480, justify=tk.LEFT).grid( row=8, column=0, columnspan=3, sticky=tk.W, pady=(12, 0) diff --git a/tests/test_gui_smoke.py b/tests/test_gui_smoke.py index 179493b..7bdbc34 100644 --- a/tests/test_gui_smoke.py +++ b/tests/test_gui_smoke.py @@ -106,3 +106,86 @@ def test_speak_tab_file_row_and_player_window_follow_their_toggles() -> None: assert not app._player_window_cb.instate(["disabled"]) finally: app.root.destroy() + + +def test_setup_tab_fits_at_minimum_size() -> None: + """At the window's minimum size, the last Setup step must still be on screen.""" + tk = pytest.importorskip("tkinter") + try: + probe = tk.Tk() + probe.destroy() + except tk.TclError: + pytest.skip("no display available") + + from tkinter import ttk + + from TTS_ka import gui + + with patch("TTS_ka.deps.collect_dep_rows", return_value=[]), \ + patch("TTS_ka.deps.format_dep_report", return_value="ok"), \ + patch("TTS_ka.native_hotkeys.NativeHotkeyManager", MagicMock()), \ + patch.object(gui.TTSSpeakApp, "_run_setup_check_async"): + app = gui.TTSSpeakApp({"lang": "en"}) + try: + w, h = app.root.minsize() + app.root.geometry(f"{w}x{h}") + app.root.update() + tab = app.root.nametowidget(app._nb.tabs()[0]) + go = next( + c for c in tab.winfo_children() + if isinstance(c, ttk.Button) and c.cget("text").startswith("Go to Speak") + ) + assert go.winfo_y() + go.winfo_height() <= tab.winfo_height() + finally: + app.root.destroy() + + +def test_no_tab_cuts_off_text_at_the_minimum_window() -> None: + """At the minimum size every label / button / check / radio gets its full size inside its tab. + + Text boxes and entries are left out: they are meant to shrink. + """ + tk = pytest.importorskip("tkinter") + try: + probe = tk.Tk() + probe.destroy() + except tk.TclError: + pytest.skip("no display available") + + from TTS_ka import gui + + fixed = {"TLabel", "TButton", "TCheckbutton", "TRadiobutton"} + + def descendants(w): + for c in w.winfo_children(): + yield c + yield from descendants(c) + + with patch("TTS_ka.deps.collect_dep_rows", return_value=[]), \ + patch("TTS_ka.deps.format_dep_report", return_value="ok"), \ + patch("TTS_ka.native_hotkeys.NativeHotkeyManager", MagicMock()), \ + patch.object(gui.TTSSpeakApp, "_run_setup_check_async"): + app = gui.TTSSpeakApp({"lang": "en"}) + try: + w, h = app.root.minsize() + app.root.geometry(f"{w}x{h}") + cut = [] + for tab_id in app._nb.tabs(): + app._nb.select(tab_id) + app.root.update() + tab = app.root.nametowidget(tab_id) + right = tab.winfo_rootx() + tab.winfo_width() + bottom = tab.winfo_rooty() + tab.winfo_height() + for c in descendants(tab): + if c.winfo_class() not in fixed or not c.winfo_ismapped(): + continue + if ( + c.winfo_width() < c.winfo_reqwidth() + or c.winfo_height() < c.winfo_reqheight() + or c.winfo_rootx() + c.winfo_width() > right + or c.winfo_rooty() + c.winfo_height() > bottom + ): + cut.append((app._nb.tab(tab_id, "text"), c.winfo_class(), str(c.cget("text"))[:40])) + assert not cut, cut + finally: + app.root.destroy()