diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 8c4678b4b23c77..d0b79a59e17527 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -1544,7 +1544,7 @@ Base and mixin classes This is typically used to wait for the user to finish interacting with a dialog box. - .. method:: wait_visibility(window=None) + .. method:: wait_visibility(window=None, *, timeout=None) Wait until the visibility state of *window* changes, for example when it first appears on the screen, continuing to process events in the @@ -1553,6 +1553,13 @@ Base and mixin classes This is typically used to wait for a newly created window to become visible before acting on it. + If *timeout* is given, it is the maximum time to wait in seconds. + Return ``True`` once *window* is viewable, or ``False`` if the timeout elapsed before that (or *window* was destroyed while waiting). + Without a *timeout* the call blocks until the visibility of *window* changes and always returns ``True``. + + .. versionchanged:: next + Added the *timeout* parameter and the return value. + The methods with the ``focus_`` prefix manage the keyboard focus. .. method:: focus_set() diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 8407d0df325619..6849d1f996d883 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -371,6 +371,11 @@ tkinter ttk version, and accepts mappings of button options as *buttons* entries. (Contributed by Serhiy Storchaka in :gh:`59396`.) +* The :meth:`!tkinter.Misc.wait_visibility` method now accepts an optional + keyword-only *timeout* argument that bounds how long it waits, instead of + blocking indefinitely. + (Contributed by Serhiy Storchaka in :gh:`86657`.) + xml --- diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index e6221f7089704d..957948ede46c5b 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -3,6 +3,7 @@ import platform import sys import textwrap +import time import unittest import weakref import tkinter @@ -611,6 +612,21 @@ def test_wait_window(self): self.root.wait_window(top) # Returns once the window is destroyed. self.assertFalse(top.winfo_exists()) + def test_wait_visibility_timeout(self): + # The widget becomes viewable before the timeout elapses. + top = tkinter.Toplevel(self.root) + self.assertIs( + self.root.wait_visibility(top, timeout=support.SHORT_TIMEOUT), True) + self.assertTrue(top.winfo_viewable()) + # The widget never becomes viewable: give up instead of blocking. + hidden = tkinter.Toplevel(self.root) + hidden.withdraw() + start = time.monotonic() + self.assertIs(self.root.wait_visibility(hidden, timeout=0.2), False) + self.assertGreaterEqual(time.monotonic() - start, 0.2) + self.assertFalse(hidden.winfo_viewable()) + hidden.destroy() + def test_tk_focusFollowsMouse(self): self.root.tk_focusFollowsMouse() # No exception. diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 83d11a7695ffbe..416609b17343ed 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -834,14 +834,49 @@ def wait_window(self, window=None): window = self self.tk.call('tkwait', 'window', window._w) - def wait_visibility(self, window=None): + def wait_visibility(self, window=None, *, timeout=None): """Wait until the visibility of a WIDGET changes (e.g. it appears). - If no parameter is given self is used.""" + If no parameter is given self is used. + + If timeout is given, it specifies the maximum time to wait in + seconds. Return True once the widget is viewable, or False if the + timeout elapsed before that (or the widget was destroyed while + waiting). Without a timeout the call blocks until the visibility of + the widget changes and always returns True.""" if window is None: window = self - self.tk.call('tkwait', 'visibility', window._w) + if timeout is None: + self.tk.call('tkwait', 'visibility', window._w) + return True + + if not window.winfo_exists(): + return False + if window.winfo_viewable(): + return True + timed_out = [] + # A one-shot timer guarantees that dooneevent() wakes up at the + # deadline even if no other event arrives. + timer = self.after(max(int(timeout * 1000), 1), + lambda: timed_out.append(True)) + try: + while not timed_out: + self.tk.dooneevent(_tkinter.ALL_EVENTS) + if not window.winfo_exists(): + return False + if window.winfo_viewable(): + return True + return False + except TclError: + # the widget or application was torn down + return False + finally: + # Cleanup may fail if the application was torn down. + try: + self.after_cancel(timer) + except TclError: + pass def setvar(self, name, value): """Set Tcl variable NAME to VALUE.""" diff --git a/Misc/NEWS.d/next/Library/2026-06-29-11-44-50.gh-issue-86657.Vz7nPq.rst b/Misc/NEWS.d/next/Library/2026-06-29-11-44-50.gh-issue-86657.Vz7nPq.rst new file mode 100644 index 00000000000000..02dfa0453fc14d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-29-11-44-50.gh-issue-86657.Vz7nPq.rst @@ -0,0 +1,3 @@ +Add an optional *timeout* parameter to :meth:`tkinter.Misc.wait_visibility`. +If the widget does not become viewable within the given time, the call +returns ``False`` instead of blocking indefinitely.