diff --git a/Lib/test/test_tkinter/test_simpledialog.py b/Lib/test/test_tkinter/test_simpledialog.py index 943d6e113ed34c..3da9375158bd02 100644 --- a/Lib/test/test_tkinter/test_simpledialog.py +++ b/Lib/test/test_tkinter/test_simpledialog.py @@ -1,3 +1,4 @@ +import sys import unittest import tkinter from tkinter import messagebox, ttk @@ -245,6 +246,26 @@ def test_go(self): d.root.after(1, lambda: d._buttons[0].invoke()) self.assertEqual(d.go(), 0) + def test_go_foreign_grab(self): + # gh-157676: the grab and the focus can be in a window which was not + # created by tkinter, such as a native message box; they must be + # restored after the dialog. + tk = self.root.tk + tk.call('toplevel', '.foreign') + self.addCleanup(tk.call, 'destroy', '.foreign') + tk.call('wm', 'deiconify', '.foreign') + tk.call('update') + tk.call('grab', 'set', '.foreign') + tk.call('focus', '-force', '.foreign') + d = self.create() + d.root.after(1, lambda: d._buttons[0].invoke()) + self.assertEqual(d.go(), 0) + self.assertEqual(tk.call('grab', 'current', self.root._w), '.foreign') + # On Windows the application can lose the focus when the dialog is + # destroyed, and then "focus" returns an empty string. + if sys.platform != 'win32': + self.assertEqual(tk.call('focus'), '.foreign') + class DialogTest(AbstractDialogTest, unittest.TestCase): # Dialog's button box is modelled on tk::MessageBox. diff --git a/Lib/tkinter/simpledialog.py b/Lib/tkinter/simpledialog.py index 1b0cf51317ee08..caef785f936db3 100644 --- a/Lib/tkinter/simpledialog.py +++ b/Lib/tkinter/simpledialog.py @@ -722,12 +722,13 @@ def askstring(title, prompt, **kw): @contextlib.contextmanager def _temp_grab_focus(grab, focus=None, destroy=True): - old_focus = grab.focus_get() - old_grab = grab.grab_current() - if old_grab is not None and old_grab.winfo_exists(): - old_status = old_grab.grab_status() - else: - old_status = None + # Use Tcl window names rather than widgets, because the focus and the + # grab can be in a window which was not created by tkinter, such as + # a native message box. + tk = grab.tk + old_focus = tk.call('focus') + old_grab = tk.call('grab', 'current', grab._w) + old_status = tk.call('grab', 'status', old_grab) if old_grab else None # The "grab" command will fail if another application # already holds the grab. So catch it. try: @@ -741,9 +742,11 @@ def _temp_grab_focus(grab, focus=None, destroy=True): yield finally: - if old_focus is not None: + # The old windows and even the whole application can be destroyed + # by now, so catch errors. + if old_focus: try: - old_focus.focus_set() + tk.call('focus', old_focus) except TclError: pass try: @@ -755,15 +758,17 @@ def _temp_grab_focus(grab, focus=None, destroy=True): grab.destroy() except TclError: pass - if (old_grab is not None and old_grab.winfo_exists() - and old_grab.winfo_ismapped()): - # The "grab" command will fail if another application - # already holds the grab. So catch it. + if old_grab: + # Do not restore the grab if the window is no longer mapped: + # a local grab on an unmapped window would block the application. + # The "grab" command will fail if another application already + # holds the grab on a window with the same name. So catch it. try: - if old_status == 'global': - old_grab.grab_set_global() - else: - old_grab.grab_set() + if grab.getboolean(tk.call('winfo', 'ismapped', old_grab)): + if old_status == 'global': + tk.call('grab', 'set', '-global', old_grab) + else: + tk.call('grab', 'set', old_grab) except TclError: pass