Skip to content

Commit 53ec4cf

Browse files
gh-71808: Rebuild IDLE query dialogs on tkinter.simpledialog.Dialog
Subclass Dialog instead of reimplementing its modal machinery. The query dialogs gain its keyboard equivalents -- <Return>, <Escape>, and Alt-underlined-letter button accelerators (gh-71807).
1 parent 2303eea commit 53ec4cf

4 files changed

Lines changed: 81 additions & 99 deletions

File tree

Lib/idlelib/idle_test/test_query.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class QueryTest(unittest.TestCase):
2727
class Dummy_Query:
2828
# Test the following Query methods.
2929
entry_ok = query.Query.entry_ok
30-
ok = query.Query.ok
30+
validate = query.Query.validate
3131
cancel = query.Query.cancel
3232
# Add attributes and initialization needed for tests.
3333
def __init__(self, dummy_entry):
@@ -53,18 +53,16 @@ def test_entry_ok_good(self):
5353
Equal((dialog.result, dialog.destroyed), (None, False))
5454
Equal(dialog.entry_error['text'], '')
5555

56-
def test_ok_blank(self):
56+
def test_validate_blank(self):
5757
dialog = self.Dummy_Query('')
58-
dialog.entry.focus_set = mock.Mock()
59-
self.assertEqual(dialog.ok(), None)
60-
self.assertTrue(dialog.entry.focus_set.called)
61-
del dialog.entry.focus_set
58+
self.assertFalse(dialog.validate())
6259
self.assertEqual((dialog.result, dialog.destroyed), (None, False))
6360

64-
def test_ok_good(self):
61+
def test_validate_good(self):
6562
dialog = self.Dummy_Query('good')
66-
self.assertEqual(dialog.ok(), None)
67-
self.assertEqual((dialog.result, dialog.destroyed), ('good', True))
63+
self.assertTrue(dialog.validate())
64+
# validate stores the result; Dialog.ok destroys the window.
65+
self.assertEqual((dialog.result, dialog.destroyed), ('good', False))
6866

6967
def test_cancel(self):
7068
dialog = self.Dummy_Query('does not matter')

Lib/idlelib/query.py

Lines changed: 63 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,26 @@
2424
import shlex
2525
from sys import executable, platform # Platform is set for one test.
2626

27-
from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
28-
from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
27+
from tkinter import StringVar, BooleanVar, W, S, EW, LEFT
28+
from tkinter.ttk import Button, Checkbutton, Entry, Label
2929
from tkinter import filedialog
3030
from tkinter.font import Font
31-
from tkinter.simpledialog import _setup_dialog
31+
from tkinter.simpledialog import Dialog
3232

33-
class Query(Toplevel):
33+
class Query(Dialog):
3434
"""Base class for getting verified answer from a user.
3535
3636
For this base class, accept any non-blank string.
37+
Built on tkinter.simpledialog.Dialog, which provides the modal
38+
behavior and the OK/Cancel buttons (with <Return>, <Escape>, and
39+
Alt-underline keyboard equivalents).
3740
"""
3841
def __init__(self, parent, title, message, *, text0='', used_names={},
3942
_htest=False, _utest=False):
4043
"""Create modal popup, return when destroyed.
4144
42-
Additional subclass init must be done before this unless
43-
_utest=True is passed to suppress wait_window().
45+
Additional subclass init must be done before calling this
46+
unless _utest=True is passed to suppress wait_window().
4447
4548
title - string, title of popup dialog
4649
message - string, informational message to display
@@ -49,78 +52,56 @@ def __init__(self, parent, title, message, *, text0='', used_names={},
4952
_htest - bool, change box location when running htest
5053
_utest - bool, leave window hidden and not modal
5154
"""
52-
self.parent = parent # Needed for Font call.
5355
self.message = message
5456
self.text0 = text0
5557
self.used_names = used_names
58+
self._htest = _htest
59+
self._utest = _utest
60+
super().__init__(parent, title, use_ttk=True)
5661

57-
Toplevel.__init__(self, parent)
58-
self.withdraw() # Hide while configuring, especially geometry.
59-
self.title(title)
60-
self.transient(parent)
61-
if not _utest: # Otherwise fail when directly run unittest.
62-
self.grab_set()
62+
def _show_modal(self):
63+
"Suppress the modal wait when unit testing."
64+
if not self._utest:
65+
super()._show_modal()
6366

64-
_setup_dialog(self)
65-
if self._windowingsystem == 'aqua':
66-
self.bind("<Command-.>", self.cancel)
67-
self.bind('<Key-Escape>', self.cancel)
68-
self.protocol("WM_DELETE_WINDOW", self.cancel)
69-
self.bind('<Key-Return>', self.ok)
70-
self.bind("<KP_Enter>", self.ok)
71-
72-
self.create_widgets()
73-
self.update_idletasks() # Need here for winfo_reqwidth below.
74-
self.geometry( # Center dialog over parent (or below htest box).
75-
"+%d+%d" % (
76-
parent.winfo_rootx() +
77-
(parent.winfo_width()/2 - self.winfo_reqwidth()/2),
78-
parent.winfo_rooty() +
79-
((parent.winfo_height()/2 - self.winfo_reqheight()/2)
80-
if not _htest else 150)
81-
) )
82-
self.resizable(height=False, width=False)
83-
84-
if not _utest:
85-
self.deiconify() # Unhide now that geometry set.
86-
self.entry.focus_set()
87-
self.wait_window()
88-
89-
def create_widgets(self, ok_text='OK'): # Do not replace.
90-
"""Create entry (rows, extras, buttons.
67+
def body(self, master): # Do not replace.
68+
"""Create entry widgets; return the entry for initial focus.
9169
9270
Entry stuff on rows 0-2, spanning cols 0-2.
93-
Buttons on row 99, cols 1, 2.
71+
Subclass extras (create_extra) go on rows 10-12.
9472
"""
9573
# Bind to self the widgets needed for entry_ok or unittest.
96-
self.frame = frame = Frame(self, padding=10)
97-
frame.grid(column=0, row=0, sticky='news')
98-
frame.grid_columnconfigure(0, weight=1)
74+
self.frame = master
75+
master.columnconfigure(0, weight=1)
9976

100-
entrylabel = Label(frame, anchor='w', justify='left',
101-
text=self.message)
77+
entrylabel = Label(master, anchor=W, justify=LEFT, text=self.message)
10278
self.entryvar = StringVar(self, self.text0)
103-
self.entry = Entry(frame, width=30, textvariable=self.entryvar)
79+
self.entry = Entry(master, width=30, textvariable=self.entryvar)
10480
self.error_font = Font(name='TkCaptionFont',
10581
exists=True, root=self.parent)
106-
self.entry_error = Label(frame, text=' ', foreground='red',
82+
self.entry_error = Label(master, text=' ', foreground='red',
10783
font=self.error_font)
10884
# Display or blank error by setting ['text'] =.
109-
entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
110-
self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
111-
pady=[10,0])
112-
self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
113-
sticky=W+E)
85+
entrylabel.grid(column=0, row=0, columnspan=3, padx='2m', pady='2m',
86+
sticky=EW)
87+
self.entry.grid(column=0, row=1, columnspan=3, padx='2m',
88+
pady=(0, '2m'), sticky=EW)
89+
self.entry_error.grid(column=0, row=2, columnspan=3, padx='2m',
90+
pady=(0, '2m'), sticky=EW)
11491

11592
self.create_extra()
11693

117-
self.button_ok = Button(
118-
frame, text=ok_text, default='active', command=self.ok)
119-
self.button_cancel = Button(
120-
frame, text='Cancel', command=self.cancel)
94+
self.resizable(height=False, width=False)
95+
if self._windowingsystem == 'aqua':
96+
self.bind("<Command-.>", self.cancel)
97+
self.bind("<KP_Enter>", self.ok)
98+
return self.entry
12199

122-
self.button_ok.grid(column=1, row=99, padx=5)
123-
self.button_cancel.grid(column=2, row=99, padx=5)
100+
def buttonbox(self): # Do not replace.
101+
"Add the standard buttons and expose them for unittest."
102+
super().buttonbox()
103+
self.button_ok = self.nametowidget('ok')
104+
self.button_cancel = self.nametowidget('cancel')
124105

125106
def create_extra(self): pass # Override to add widgets.
126107

@@ -136,28 +117,18 @@ def entry_ok(self): # Example: usually replace.
136117
return None
137118
return entry
138119

139-
def ok(self, event=None): # Do not replace.
140-
'''If entry is valid, bind it to 'result' and destroy tk widget.
120+
def validate(self): # Do not replace.
121+
"""If entry is valid, store it in 'result' and return True.
141122
142-
Otherwise leave dialog open for user to correct entry or cancel.
143-
'''
123+
Otherwise show the error and leave the dialog open (Dialog.ok
124+
puts the focus back on the entry).
125+
"""
144126
self.entry_error['text'] = ''
145127
entry = self.entry_ok()
146-
if entry is not None:
147-
self.result = entry
148-
self.destroy()
149-
else:
150-
# [Ok] moves focus. (<Return> does not.) Move it back.
151-
self.entry.focus_set()
152-
153-
def cancel(self, event=None): # Do not replace.
154-
"Set dialog result to None and destroy tk widget."
155-
self.result = None
156-
self.destroy()
157-
158-
def destroy(self):
159-
self.grab_release()
160-
super().destroy()
128+
if entry is None:
129+
return False
130+
self.result = entry
131+
return True
161132

162133

163134
class SectionName(Query):
@@ -260,9 +231,9 @@ def __init__(self, parent, title, *, menuitem='', filepath='',
260231
used_names=used_names, _htest=_htest, _utest=_utest)
261232

262233
def create_extra(self):
263-
"Add path widjets to rows 10-12."
234+
"Add path widgets to rows 10-12."
264235
frame = self.frame
265-
pathlabel = Label(frame, anchor='w', justify='left',
236+
pathlabel = Label(frame, anchor=W, justify=LEFT,
266237
text='Help File Path: Enter URL or browse for file')
267238
self.pathvar = StringVar(self, self.filepath)
268239
self.path = Entry(frame, textvariable=self.pathvar, width=40)
@@ -271,13 +242,14 @@ def create_extra(self):
271242
self.path_error = Label(frame, text=' ', foreground='red',
272243
font=self.error_font)
273244

274-
pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
275-
sticky=W)
276-
self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
277-
pady=[10,0])
278-
browse.grid(column=2, row=11, padx=5, sticky=W+S)
279-
self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
280-
sticky=W+E)
245+
pathlabel.grid(column=0, row=10, columnspan=3, padx='2m',
246+
pady=(0, '2m'), sticky=EW)
247+
self.path.grid(column=0, row=11, columnspan=2, padx='2m',
248+
pady=(0, '2m'), sticky=EW)
249+
browse.grid(column=2, row=11, padx=(0, '2m'), pady=(0, '2m'),
250+
sticky=W+S)
251+
self.path_error.grid(column=0, row=12, columnspan=3, padx='2m',
252+
pady=(0, '2m'), sticky=EW)
281253

282254
def askfilename(self, filetypes, initdir, initfile): # htest #
283255
# Extracted from browse_file so can mock for unittests.
@@ -361,9 +333,10 @@ def create_extra(self):
361333
self.args_error = Label(frame, text=' ', foreground='red',
362334
font=self.error_font)
363335

364-
restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
365-
self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
366-
sticky='we')
336+
restart.grid(column=0, row=10, columnspan=3, padx='2m',
337+
pady=(0, '2m'), sticky=W)
338+
self.args_error.grid(column=0, row=12, columnspan=3, padx='2m',
339+
pady=(0, '2m'), sticky=EW)
367340

368341
def cli_args_ok(self):
369342
"Return command line arg list or None if error."

Lib/tkinter/simpledialog.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,13 @@ def __init__(self, parent, title=None, *, use_ttk=False):
328328

329329
_place_window(self, parent)
330330

331+
self._show_modal()
332+
333+
def _show_modal(self):
334+
'''Grab the focus and wait until the dialog is destroyed.
335+
336+
Override to show the dialog without blocking (e.g. for testing).
337+
'''
331338
# wait for window to appear on screen before calling grab_set
332339
self.wait_visibility()
333340
# Dialog destroys itself in ok()/cancel(), so let _temp_grab_focus
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Rebuild the IDLE query dialogs (used for Open Module, custom Run, and new
2+
config-section names) on ``tkinter.simpledialog.Dialog``. They gain its
3+
standard keyboard behavior: ``Return``, ``Escape``, and Alt-underlined-letter
4+
button accelerators.

0 commit comments

Comments
 (0)