blob: 015fc7ade459dbde432759a1b66b19ab32e5f6b5 [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
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040031
32class Query(Toplevel):
33 """Base class for getting verified answer from a user.
34
35 For this base class, accept any non-blank string.
36 """
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040037 def __init__(self, parent, title, message, *, text0='', used_names={},
38 _htest=False, _utest=False):
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040039 """Create modal popup, return when destroyed.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040040
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040041 Additional subclass init must be done before this unless
42 _utest=True is passed to suppress wait_window().
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040043
44 title - string, title of popup dialog
45 message - string, informational message to display
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040046 text0 - initial value for entry
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040047 used_names - names already in use
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040048 _htest - bool, change box location when running htest
49 _utest - bool, leave window hidden and not modal
50 """
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040051 self.parent = parent # Needed for Font call.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040052 self.message = message
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040053 self.text0 = text0
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040054 self.used_names = used_names
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040055
56 Toplevel.__init__(self, parent)
57 self.withdraw() # Hide while configuring, especially geometry.
58 self.title(title)
Terry Jan Reedy65db8542016-08-10 12:50:16 -040059 self.transient(parent)
E-Paine8ab77c62020-06-28 08:02:47 +020060 if not _utest: # Otherwise fail when directly run unittest.
61 self.grab_set()
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040062
Terry Jan Reedy65db8542016-08-10 12:50:16 -040063 windowingsystem = self.tk.call('tk', 'windowingsystem')
64 if windowingsystem == 'aqua':
65 try:
66 self.tk.call('::tk::unsupported::MacWindowStyle', 'style',
67 self._w, 'moveableModal', '')
68 except:
69 pass
70 self.bind("<Command-.>", self.cancel)
71 self.bind('<Key-Escape>', self.cancel)
72 self.protocol("WM_DELETE_WINDOW", self.cancel)
73 self.bind('<Key-Return>', self.ok)
74 self.bind("<KP_Enter>", self.ok)
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040075
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040076 self.create_widgets()
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040077 self.update_idletasks() # Need here for winfo_reqwidth below.
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040078 self.geometry( # Center dialog over parent (or below htest box).
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040079 "+%d+%d" % (
80 parent.winfo_rootx() +
81 (parent.winfo_width()/2 - self.winfo_reqwidth()/2),
82 parent.winfo_rooty() +
83 ((parent.winfo_height()/2 - self.winfo_reqheight()/2)
84 if not _htest else 150)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040085 ) )
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040086 self.resizable(height=False, width=False)
87
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040088 if not _utest:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040089 self.deiconify() # Unhide now that geometry set.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040090 self.wait_window()
91
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040092 def create_widgets(self, ok_text='OK'): # Do not replace.
93 """Create entry (rows, extras, buttons.
94
95 Entry stuff on rows 0-2, spanning cols 0-2.
96 Buttons on row 99, cols 1, 2.
97 """
98 # Bind to self the widgets needed for entry_ok or unittest.
Terry Jan Reedy65db8542016-08-10 12:50:16 -040099 self.frame = frame = Frame(self, padding=10)
100 frame.grid(column=0, row=0, sticky='news')
101 frame.grid_columnconfigure(0, weight=1)
102
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400103 entrylabel = Label(frame, anchor='w', justify='left',
104 text=self.message)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400105 self.entryvar = StringVar(self, self.text0)
106 self.entry = Entry(frame, width=30, textvariable=self.entryvar)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400107 self.entry.focus_set()
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400108 self.error_font = Font(name='TkCaptionFont',
109 exists=True, root=self.parent)
110 self.entry_error = Label(frame, text=' ', foreground='red',
111 font=self.error_font)
Terry Jan Reedye53a3932020-03-09 01:38:07 -0400112 # Display or blank error by setting ['text'] =.
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400113 entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
114 self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
115 pady=[10,0])
116 self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
117 sticky=W+E)
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400118
119 self.create_extra()
120
121 self.button_ok = Button(
122 frame, text=ok_text, default='active', command=self.ok)
123 self.button_cancel = Button(
124 frame, text='Cancel', command=self.cancel)
125
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400126 self.button_ok.grid(column=1, row=99, padx=5)
127 self.button_cancel.grid(column=2, row=99, padx=5)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400128
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400129 def create_extra(self): pass # Override to add widgets.
130
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400131 def showerror(self, message, widget=None):
132 #self.bell(displayof=self)
133 (widget or self.entry_error)['text'] = 'ERROR: ' + message
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400134
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400135 def entry_ok(self): # Example: usually replace.
136 "Return non-blank entry or None."
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400137 entry = self.entry.get().strip()
138 if not entry:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400139 self.showerror('blank line.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400140 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400141 return entry
142
143 def ok(self, event=None): # Do not replace.
144 '''If entry is valid, bind it to 'result' and destroy tk widget.
145
146 Otherwise leave dialog open for user to correct entry or cancel.
147 '''
Terry Jan Reedye53a3932020-03-09 01:38:07 -0400148 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400149 entry = self.entry_ok()
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400150 if entry is not None:
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400151 self.result = entry
152 self.destroy()
153 else:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400154 # [Ok] moves focus. (<Return> does not.) Move it back.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400155 self.entry.focus_set()
156
157 def cancel(self, event=None): # Do not replace.
158 "Set dialog result to None and destroy tk widget."
159 self.result = None
160 self.destroy()
161
Tal Einat10ea9402018-08-02 09:18:29 +0300162 def destroy(self):
163 self.grab_release()
164 super().destroy()
165
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400166
167class SectionName(Query):
168 "Get a name for a config file section name."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400169 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400170
171 def __init__(self, parent, title, message, used_names,
172 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400173 super().__init__(parent, title, message, used_names=used_names,
174 _htest=_htest, _utest=_utest)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400175
176 def entry_ok(self):
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400177 "Return sensible ConfigParser section name or None."
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400178 name = self.entry.get().strip()
179 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400180 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400181 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400182 elif len(name)>30:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400183 self.showerror('name is longer than 30 characters.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400184 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400185 elif name in self.used_names:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400186 self.showerror('name is already in use.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400187 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400188 return name
189
190
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400191class ModuleName(Query):
192 "Get a module name for Open Module menu entry."
193 # Used in open_module (editor.EditorWindow until move to iobinding).
194
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400195 def __init__(self, parent, title, message, text0,
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400196 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400197 super().__init__(parent, title, message, text0=text0,
198 _htest=_htest, _utest=_utest)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400199
200 def entry_ok(self):
201 "Return entered module name as file path or None."
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400202 name = self.entry.get().strip()
203 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400204 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400205 return None
206 # XXX Ought to insert current file's directory in front of path.
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400207 try:
208 spec = importlib.util.find_spec(name)
209 except (ValueError, ImportError) as msg:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400210 self.showerror(str(msg))
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400211 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400212 if spec is None:
E-Paine8ab77c62020-06-28 08:02:47 +0200213 self.showerror("module not found.")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400214 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400215 if not isinstance(spec.loader, importlib.abc.SourceLoader):
E-Paine8ab77c62020-06-28 08:02:47 +0200216 self.showerror("not a source-based module.")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400217 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400218 try:
219 file_path = spec.loader.get_filename(name)
220 except AttributeError:
E-Paine8ab77c62020-06-28 08:02:47 +0200221 self.showerror("loader does not support get_filename.")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400222 return None
E-Paine8ab77c62020-06-28 08:02:47 +0200223 except ImportError:
224 # Some special modules require this (e.g. os.path)
225 try:
226 file_path = spec.loader.get_filename()
227 except TypeError:
228 self.showerror("loader failed to get filename.")
229 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400230 return file_path
231
232
Terry Jan Reedy363fab82020-03-09 16:51:20 -0400233class Goto(Query):
234 "Get a positive line number for editor Go To Line."
235 # Used in editor.EditorWindow.goto_line_event.
236
237 def entry_ok(self):
238 try:
239 lineno = int(self.entry.get())
240 except ValueError:
241 self.showerror('not a base 10 integer.')
242 return None
243 if lineno <= 0:
244 self.showerror('not a positive integer.')
245 return None
246 return lineno
247
248
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400249class HelpSource(Query):
250 "Get menu name and help source for Help menu."
251 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
252
253 def __init__(self, parent, title, *, menuitem='', filepath='',
254 used_names={}, _htest=False, _utest=False):
255 """Get menu entry and url/local file for Additional Help.
256
257 User enters a name for the Help resource and a web url or file
258 name. The user can browse for the file.
259 """
260 self.filepath = filepath
261 message = 'Name for item on Help menu:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400262 super().__init__(
263 parent, title, message, text0=menuitem,
264 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400265
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400266 def create_extra(self):
267 "Add path widjets to rows 10-12."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400268 frame = self.frame
269 pathlabel = Label(frame, anchor='w', justify='left',
270 text='Help File Path: Enter URL or browse for file')
271 self.pathvar = StringVar(self, self.filepath)
272 self.path = Entry(frame, textvariable=self.pathvar, width=40)
273 browse = Button(frame, text='Browse', width=8,
274 command=self.browse_file)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400275 self.path_error = Label(frame, text=' ', foreground='red',
276 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400277
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400278 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
279 sticky=W)
280 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
281 pady=[10,0])
282 browse.grid(column=2, row=11, padx=5, sticky=W+S)
283 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
284 sticky=W+E)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400285
286 def askfilename(self, filetypes, initdir, initfile): # htest #
287 # Extracted from browse_file so can mock for unittests.
288 # Cannot unittest as cannot simulate button clicks.
289 # Test by running htest, such as by running this file.
290 return filedialog.Open(parent=self, filetypes=filetypes)\
291 .show(initialdir=initdir, initialfile=initfile)
292
293 def browse_file(self):
294 filetypes = [
295 ("HTML Files", "*.htm *.html", "TEXT"),
296 ("PDF Files", "*.pdf", "TEXT"),
297 ("Windows Help Files", "*.chm"),
298 ("Text Files", "*.txt", "TEXT"),
299 ("All Files", "*")]
300 path = self.pathvar.get()
301 if path:
302 dir, base = os.path.split(path)
303 else:
304 base = None
305 if platform[:3] == 'win':
306 dir = os.path.join(os.path.dirname(executable), 'Doc')
307 if not os.path.isdir(dir):
308 dir = os.getcwd()
309 else:
310 dir = os.getcwd()
311 file = self.askfilename(filetypes, dir, base)
312 if file:
313 self.pathvar.set(file)
314
315 item_ok = SectionName.entry_ok # localize for test override
316
317 def path_ok(self):
318 "Simple validity check for menu file path"
319 path = self.path.get().strip()
320 if not path: #no path specified
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400321 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400322 return None
323 elif not path.startswith(('www.', 'http')):
324 if path[:5] == 'file:':
325 path = path[5:]
326 if not os.path.exists(path):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400327 self.showerror('help file path does not exist.',
328 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400329 return None
330 if platform == 'darwin': # for Mac Safari
331 path = "file://" + path
332 return path
333
334 def entry_ok(self):
335 "Return apparently valid (name, path) or None"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400336 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400337 name = self.item_ok()
338 path = self.path_ok()
339 return None if name is None or path is None else (name, path)
340
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400341class CustomRun(Query):
342 """Get settings for custom run of module.
343
344 1. Command line arguments to extend sys.argv.
345 2. Whether to restart Shell or not.
346 """
347 # Used in runscript.run_custom_event
348
Ngalim Siregar35b87e62019-07-21 22:37:28 +0700349 def __init__(self, parent, title, *, cli_args=[],
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400350 _htest=False, _utest=False):
Ngalim Siregar35b87e62019-07-21 22:37:28 +0700351 """cli_args is a list of strings.
352
353 The list is assigned to the default Entry StringVar.
354 The strings are displayed joined by ' ' for display.
355 """
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400356 message = 'Command Line Arguments for sys.argv:'
357 super().__init__(
358 parent, title, message, text0=cli_args,
359 _htest=_htest, _utest=_utest)
360
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400361 def create_extra(self):
362 "Add run mode on rows 10-12."
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400363 frame = self.frame
364 self.restartvar = BooleanVar(self, value=True)
365 restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
366 offvalue=False, text='Restart shell')
367 self.args_error = Label(frame, text=' ', foreground='red',
368 font=self.error_font)
369
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400370 restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400371 self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
372 sticky='we')
373
374 def cli_args_ok(self):
375 "Validity check and parsing for command line arguments."
376 cli_string = self.entry.get().strip()
377 try:
378 cli_args = shlex.split(cli_string, posix=True)
379 except ValueError as err:
380 self.showerror(str(err))
381 return None
382 return cli_args
383
384 def entry_ok(self):
E-Paine8ab77c62020-06-28 08:02:47 +0200385 "Return apparently valid (cli_args, restart) or None."
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400386 cli_args = self.cli_args_ok()
387 restart = self.restartvar.get()
388 return None if cli_args is None else (cli_args, restart)
389
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400390
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400391if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400392 from unittest import main
393 main('idlelib.idle_test.test_query', verbosity=2, exit=False)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400394
395 from idlelib.idle_test.htest import run
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400396 run(Query, HelpSource, CustomRun)