blob: fefa5aac1b7f58a78931339719817d22a4ae35b0 [file] [log] [blame]
Terry Jan Reedy68a53c52016-06-26 22:05:10 -04001"""
2Dialogs that query users and verify the answer before accepting.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -04003
4Query is the generic base class for a popup dialog.
5The user must either enter a valid answer or close the dialog.
6Entries are validated when <Return> is entered or [Ok] is clicked.
7Entries are ignored when [Cancel] or [X] are clicked.
8The 'return value' is .result set to either a valid answer or None.
9
10Subclass SectionName gets a name for a new config file section.
11Configdialog uses it for new highlight theme and keybinding set names.
Terry Jan Reedy65db8542016-08-10 12:50:16 -040012Subclass ModuleName gets a name for File => Open Module.
13Subclass HelpSource gets menu item and path for additions to Help menu.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040014"""
15# Query and Section name result from splitting GetCfgSectionNameDialog
16# of configSectionNameDialog.py (temporarily config_sec.py) into
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040017# generic and specific parts. 3.6 only, July 2016.
18# ModuleName.entry_ok came from editor.EditorWindow.load_module.
19# HelpSource was extracted from configHelpSourceEdit.py (temporarily
20# config_help.py), with darwin code moved from ok to path_ok.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040021
E-Paine8ab77c62020-06-28 08:02:47 +020022import importlib.util, importlib.abc
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040023import os
Cheryl Sabella201bc2d2019-06-17 22:24:10 -040024import shlex
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040025from sys import executable, platform # Platform is set for one test.
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040026
Cheryl Sabella201bc2d2019-06-17 22:24:10 -040027from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
28from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040029from tkinter import filedialog
Terry Jan Reedy65db8542016-08-10 12:50:16 -040030from tkinter.font import Font
Serhiy Storchaka3bb3fb32021-04-25 13:07:58 +030031from tkinter.simpledialog import _setup_dialog
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040032
33class Query(Toplevel):
34 """Base class for getting verified answer from a user.
35
36 For this base class, accept any non-blank string.
37 """
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040038 def __init__(self, parent, title, message, *, text0='', used_names={},
39 _htest=False, _utest=False):
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040040 """Create modal popup, return when destroyed.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040041
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040042 Additional subclass init must be done before this unless
43 _utest=True is passed to suppress wait_window().
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040044
45 title - string, title of popup dialog
46 message - string, informational message to display
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040047 text0 - initial value for entry
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040048 used_names - names already in use
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040049 _htest - bool, change box location when running htest
50 _utest - bool, leave window hidden and not modal
51 """
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040052 self.parent = parent # Needed for Font call.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040053 self.message = message
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040054 self.text0 = text0
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040055 self.used_names = used_names
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040056
57 Toplevel.__init__(self, parent)
58 self.withdraw() # Hide while configuring, especially geometry.
59 self.title(title)
Terry Jan Reedy65db8542016-08-10 12:50:16 -040060 self.transient(parent)
E-Paine8ab77c62020-06-28 08:02:47 +020061 if not _utest: # Otherwise fail when directly run unittest.
62 self.grab_set()
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040063
Serhiy Storchaka3bb3fb32021-04-25 13:07:58 +030064 _setup_dialog(self)
65 if self._windowingsystem == 'aqua':
Terry Jan Reedy65db8542016-08-10 12:50:16 -040066 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)
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040071
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040072 self.create_widgets()
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040073 self.update_idletasks() # Need here for winfo_reqwidth below.
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040074 self.geometry( # Center dialog over parent (or below htest box).
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040075 "+%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)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040081 ) )
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040082 self.resizable(height=False, width=False)
83
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040084 if not _utest:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040085 self.deiconify() # Unhide now that geometry set.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040086 self.wait_window()
87
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040088 def create_widgets(self, ok_text='OK'): # Do not replace.
89 """Create entry (rows, extras, buttons.
90
91 Entry stuff on rows 0-2, spanning cols 0-2.
92 Buttons on row 99, cols 1, 2.
93 """
94 # Bind to self the widgets needed for entry_ok or unittest.
Terry Jan Reedy65db8542016-08-10 12:50:16 -040095 self.frame = frame = Frame(self, padding=10)
96 frame.grid(column=0, row=0, sticky='news')
97 frame.grid_columnconfigure(0, weight=1)
98
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040099 entrylabel = Label(frame, anchor='w', justify='left',
100 text=self.message)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400101 self.entryvar = StringVar(self, self.text0)
102 self.entry = Entry(frame, width=30, textvariable=self.entryvar)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400103 self.entry.focus_set()
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400104 self.error_font = Font(name='TkCaptionFont',
105 exists=True, root=self.parent)
106 self.entry_error = Label(frame, text=' ', foreground='red',
107 font=self.error_font)
Terry Jan Reedye53a3932020-03-09 01:38:07 -0400108 # Display or blank error by setting ['text'] =.
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400109 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)
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400114
115 self.create_extra()
116
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)
121
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400122 self.button_ok.grid(column=1, row=99, padx=5)
123 self.button_cancel.grid(column=2, row=99, padx=5)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400124
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400125 def create_extra(self): pass # Override to add widgets.
126
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400127 def showerror(self, message, widget=None):
128 #self.bell(displayof=self)
129 (widget or self.entry_error)['text'] = 'ERROR: ' + message
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400130
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400131 def entry_ok(self): # Example: usually replace.
132 "Return non-blank entry or None."
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400133 entry = self.entry.get().strip()
134 if not entry:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400135 self.showerror('blank line.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400136 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400137 return entry
138
139 def ok(self, event=None): # Do not replace.
140 '''If entry is valid, bind it to 'result' and destroy tk widget.
141
142 Otherwise leave dialog open for user to correct entry or cancel.
143 '''
Terry Jan Reedye53a3932020-03-09 01:38:07 -0400144 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400145 entry = self.entry_ok()
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400146 if entry is not None:
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400147 self.result = entry
148 self.destroy()
149 else:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400150 # [Ok] moves focus. (<Return> does not.) Move it back.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400151 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
Tal Einat10ea9402018-08-02 09:18:29 +0300158 def destroy(self):
159 self.grab_release()
160 super().destroy()
161
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400162
163class SectionName(Query):
164 "Get a name for a config file section name."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400165 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400166
167 def __init__(self, parent, title, message, used_names,
168 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400169 super().__init__(parent, title, message, used_names=used_names,
170 _htest=_htest, _utest=_utest)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400171
172 def entry_ok(self):
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400173 "Return sensible ConfigParser section name or None."
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400174 name = self.entry.get().strip()
175 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400176 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400177 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400178 elif len(name)>30:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400179 self.showerror('name is longer than 30 characters.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400180 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400181 elif name in self.used_names:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400182 self.showerror('name is already in use.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400183 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400184 return name
185
186
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400187class ModuleName(Query):
188 "Get a module name for Open Module menu entry."
189 # Used in open_module (editor.EditorWindow until move to iobinding).
190
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400191 def __init__(self, parent, title, message, text0,
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400192 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400193 super().__init__(parent, title, message, text0=text0,
194 _htest=_htest, _utest=_utest)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400195
196 def entry_ok(self):
197 "Return entered module name as file path or None."
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400198 name = self.entry.get().strip()
199 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400200 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400201 return None
202 # XXX Ought to insert current file's directory in front of path.
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400203 try:
204 spec = importlib.util.find_spec(name)
205 except (ValueError, ImportError) as msg:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400206 self.showerror(str(msg))
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400207 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400208 if spec is None:
E-Paine8ab77c62020-06-28 08:02:47 +0200209 self.showerror("module not found.")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400210 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400211 if not isinstance(spec.loader, importlib.abc.SourceLoader):
E-Paine8ab77c62020-06-28 08:02:47 +0200212 self.showerror("not a source-based module.")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400213 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400214 try:
215 file_path = spec.loader.get_filename(name)
216 except AttributeError:
E-Paine8ab77c62020-06-28 08:02:47 +0200217 self.showerror("loader does not support get_filename.")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400218 return None
E-Paine8ab77c62020-06-28 08:02:47 +0200219 except ImportError:
220 # Some special modules require this (e.g. os.path)
221 try:
222 file_path = spec.loader.get_filename()
223 except TypeError:
224 self.showerror("loader failed to get filename.")
225 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400226 return file_path
227
228
Terry Jan Reedy363fab82020-03-09 16:51:20 -0400229class Goto(Query):
230 "Get a positive line number for editor Go To Line."
231 # Used in editor.EditorWindow.goto_line_event.
232
233 def entry_ok(self):
234 try:
235 lineno = int(self.entry.get())
236 except ValueError:
237 self.showerror('not a base 10 integer.')
238 return None
239 if lineno <= 0:
240 self.showerror('not a positive integer.')
241 return None
242 return lineno
243
244
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400245class HelpSource(Query):
246 "Get menu name and help source for Help menu."
247 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
248
249 def __init__(self, parent, title, *, menuitem='', filepath='',
250 used_names={}, _htest=False, _utest=False):
251 """Get menu entry and url/local file for Additional Help.
252
253 User enters a name for the Help resource and a web url or file
254 name. The user can browse for the file.
255 """
256 self.filepath = filepath
257 message = 'Name for item on Help menu:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400258 super().__init__(
259 parent, title, message, text0=menuitem,
260 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400261
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400262 def create_extra(self):
263 "Add path widjets to rows 10-12."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400264 frame = self.frame
265 pathlabel = Label(frame, anchor='w', justify='left',
266 text='Help File Path: Enter URL or browse for file')
267 self.pathvar = StringVar(self, self.filepath)
268 self.path = Entry(frame, textvariable=self.pathvar, width=40)
269 browse = Button(frame, text='Browse', width=8,
270 command=self.browse_file)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400271 self.path_error = Label(frame, text=' ', foreground='red',
272 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400273
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400274 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)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400281
282 def askfilename(self, filetypes, initdir, initfile): # htest #
283 # Extracted from browse_file so can mock for unittests.
284 # Cannot unittest as cannot simulate button clicks.
285 # Test by running htest, such as by running this file.
286 return filedialog.Open(parent=self, filetypes=filetypes)\
287 .show(initialdir=initdir, initialfile=initfile)
288
289 def browse_file(self):
290 filetypes = [
291 ("HTML Files", "*.htm *.html", "TEXT"),
292 ("PDF Files", "*.pdf", "TEXT"),
293 ("Windows Help Files", "*.chm"),
294 ("Text Files", "*.txt", "TEXT"),
295 ("All Files", "*")]
296 path = self.pathvar.get()
297 if path:
298 dir, base = os.path.split(path)
299 else:
300 base = None
301 if platform[:3] == 'win':
302 dir = os.path.join(os.path.dirname(executable), 'Doc')
303 if not os.path.isdir(dir):
304 dir = os.getcwd()
305 else:
306 dir = os.getcwd()
307 file = self.askfilename(filetypes, dir, base)
308 if file:
309 self.pathvar.set(file)
310
311 item_ok = SectionName.entry_ok # localize for test override
312
313 def path_ok(self):
314 "Simple validity check for menu file path"
315 path = self.path.get().strip()
316 if not path: #no path specified
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400317 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400318 return None
319 elif not path.startswith(('www.', 'http')):
320 if path[:5] == 'file:':
321 path = path[5:]
322 if not os.path.exists(path):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400323 self.showerror('help file path does not exist.',
324 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400325 return None
326 if platform == 'darwin': # for Mac Safari
327 path = "file://" + path
328 return path
329
330 def entry_ok(self):
331 "Return apparently valid (name, path) or None"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400332 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400333 name = self.item_ok()
334 path = self.path_ok()
335 return None if name is None or path is None else (name, path)
336
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400337class CustomRun(Query):
338 """Get settings for custom run of module.
339
340 1. Command line arguments to extend sys.argv.
341 2. Whether to restart Shell or not.
342 """
343 # Used in runscript.run_custom_event
344
Ngalim Siregar35b87e62019-07-21 22:37:28 +0700345 def __init__(self, parent, title, *, cli_args=[],
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400346 _htest=False, _utest=False):
Ngalim Siregar35b87e62019-07-21 22:37:28 +0700347 """cli_args is a list of strings.
348
349 The list is assigned to the default Entry StringVar.
350 The strings are displayed joined by ' ' for display.
351 """
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400352 message = 'Command Line Arguments for sys.argv:'
353 super().__init__(
354 parent, title, message, text0=cli_args,
355 _htest=_htest, _utest=_utest)
356
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400357 def create_extra(self):
358 "Add run mode on rows 10-12."
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400359 frame = self.frame
360 self.restartvar = BooleanVar(self, value=True)
361 restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
362 offvalue=False, text='Restart shell')
363 self.args_error = Label(frame, text=' ', foreground='red',
364 font=self.error_font)
365
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400366 restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400367 self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
368 sticky='we')
369
370 def cli_args_ok(self):
371 "Validity check and parsing for command line arguments."
372 cli_string = self.entry.get().strip()
373 try:
374 cli_args = shlex.split(cli_string, posix=True)
375 except ValueError as err:
376 self.showerror(str(err))
377 return None
378 return cli_args
379
380 def entry_ok(self):
E-Paine8ab77c62020-06-28 08:02:47 +0200381 "Return apparently valid (cli_args, restart) or None."
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400382 cli_args = self.cli_args_ok()
383 restart = self.restartvar.get()
384 return None if cli_args is None else (cli_args, restart)
385
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400386
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400387if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400388 from unittest import main
389 main('idlelib.idle_test.test_query', verbosity=2, exit=False)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400390
391 from idlelib.idle_test.htest import run
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400392 run(Query, HelpSource, CustomRun)