blob: 2a88530b4d082a51ce189327e76394c818c6608f [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
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040022import importlib
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)
60 self.grab_set()
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040061
Terry Jan Reedy65db8542016-08-10 12:50:16 -040062 windowingsystem = self.tk.call('tk', 'windowingsystem')
63 if windowingsystem == 'aqua':
64 try:
65 self.tk.call('::tk::unsupported::MacWindowStyle', 'style',
66 self._w, 'moveableModal', '')
67 except:
68 pass
69 self.bind("<Command-.>", self.cancel)
70 self.bind('<Key-Escape>', self.cancel)
71 self.protocol("WM_DELETE_WINDOW", self.cancel)
72 self.bind('<Key-Return>', self.ok)
73 self.bind("<KP_Enter>", self.ok)
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040074
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040075 self.create_widgets()
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040076 self.update_idletasks() # Need here for winfo_reqwidth below.
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040077 self.geometry( # Center dialog over parent (or below htest box).
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040078 "+%d+%d" % (
79 parent.winfo_rootx() +
80 (parent.winfo_width()/2 - self.winfo_reqwidth()/2),
81 parent.winfo_rooty() +
82 ((parent.winfo_height()/2 - self.winfo_reqheight()/2)
83 if not _htest else 150)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040084 ) )
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040085 self.resizable(height=False, width=False)
86
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040087 if not _utest:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040088 self.deiconify() # Unhide now that geometry set.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040089 self.wait_window()
90
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -040091 def create_widgets(self, ok_text='OK'): # Do not replace.
92 """Create entry (rows, extras, buttons.
93
94 Entry stuff on rows 0-2, spanning cols 0-2.
95 Buttons on row 99, cols 1, 2.
96 """
97 # Bind to self the widgets needed for entry_ok or unittest.
Terry Jan Reedy65db8542016-08-10 12:50:16 -040098 self.frame = frame = Frame(self, padding=10)
99 frame.grid(column=0, row=0, sticky='news')
100 frame.grid_columnconfigure(0, weight=1)
101
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400102 entrylabel = Label(frame, anchor='w', justify='left',
103 text=self.message)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400104 self.entryvar = StringVar(self, self.text0)
105 self.entry = Entry(frame, width=30, textvariable=self.entryvar)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400106 self.entry.focus_set()
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400107 self.error_font = Font(name='TkCaptionFont',
108 exists=True, root=self.parent)
109 self.entry_error = Label(frame, text=' ', foreground='red',
110 font=self.error_font)
Terry Jan Reedye53a3932020-03-09 01:38:07 -0400111 # Display or blank error by setting ['text'] =.
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400112 entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
113 self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
114 pady=[10,0])
115 self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
116 sticky=W+E)
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400117
118 self.create_extra()
119
120 self.button_ok = Button(
121 frame, text=ok_text, default='active', command=self.ok)
122 self.button_cancel = Button(
123 frame, text='Cancel', command=self.cancel)
124
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400125 self.button_ok.grid(column=1, row=99, padx=5)
126 self.button_cancel.grid(column=2, row=99, padx=5)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400127
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400128 def create_extra(self): pass # Override to add widgets.
129
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400130 def showerror(self, message, widget=None):
131 #self.bell(displayof=self)
132 (widget or self.entry_error)['text'] = 'ERROR: ' + message
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400133
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400134 def entry_ok(self): # Example: usually replace.
135 "Return non-blank entry or None."
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400136 entry = self.entry.get().strip()
137 if not entry:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400138 self.showerror('blank line.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400139 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400140 return entry
141
142 def ok(self, event=None): # Do not replace.
143 '''If entry is valid, bind it to 'result' and destroy tk widget.
144
145 Otherwise leave dialog open for user to correct entry or cancel.
146 '''
Terry Jan Reedye53a3932020-03-09 01:38:07 -0400147 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400148 entry = self.entry_ok()
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400149 if entry is not None:
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400150 self.result = entry
151 self.destroy()
152 else:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400153 # [Ok] moves focus. (<Return> does not.) Move it back.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400154 self.entry.focus_set()
155
156 def cancel(self, event=None): # Do not replace.
157 "Set dialog result to None and destroy tk widget."
158 self.result = None
159 self.destroy()
160
Tal Einat10ea9402018-08-02 09:18:29 +0300161 def destroy(self):
162 self.grab_release()
163 super().destroy()
164
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400165
166class SectionName(Query):
167 "Get a name for a config file section name."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400168 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400169
170 def __init__(self, parent, title, message, used_names,
171 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400172 super().__init__(parent, title, message, used_names=used_names,
173 _htest=_htest, _utest=_utest)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400174
175 def entry_ok(self):
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400176 "Return sensible ConfigParser section name or None."
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400177 name = self.entry.get().strip()
178 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400179 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400180 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400181 elif len(name)>30:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400182 self.showerror('name is longer than 30 characters.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400183 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400184 elif name in self.used_names:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400185 self.showerror('name is already in use.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400186 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400187 return name
188
189
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400190class ModuleName(Query):
191 "Get a module name for Open Module menu entry."
192 # Used in open_module (editor.EditorWindow until move to iobinding).
193
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400194 def __init__(self, parent, title, message, text0,
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400195 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400196 super().__init__(parent, title, message, text0=text0,
197 _htest=_htest, _utest=_utest)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400198
199 def entry_ok(self):
200 "Return entered module name as file path or None."
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400201 name = self.entry.get().strip()
202 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400203 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400204 return None
205 # XXX Ought to insert current file's directory in front of path.
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400206 try:
207 spec = importlib.util.find_spec(name)
208 except (ValueError, ImportError) as msg:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400209 self.showerror(str(msg))
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400210 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400211 if spec is None:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400212 self.showerror("module not found")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400213 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400214 if not isinstance(spec.loader, importlib.abc.SourceLoader):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400215 self.showerror("not a source-based module")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400216 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400217 try:
218 file_path = spec.loader.get_filename(name)
219 except AttributeError:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400220 self.showerror("loader does not support get_filename",
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400221 parent=self)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400222 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400223 return file_path
224
225
Terry Jan Reedy363fab82020-03-09 16:51:20 -0400226class Goto(Query):
227 "Get a positive line number for editor Go To Line."
228 # Used in editor.EditorWindow.goto_line_event.
229
230 def entry_ok(self):
231 try:
232 lineno = int(self.entry.get())
233 except ValueError:
234 self.showerror('not a base 10 integer.')
235 return None
236 if lineno <= 0:
237 self.showerror('not a positive integer.')
238 return None
239 return lineno
240
241
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400242class HelpSource(Query):
243 "Get menu name and help source for Help menu."
244 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
245
246 def __init__(self, parent, title, *, menuitem='', filepath='',
247 used_names={}, _htest=False, _utest=False):
248 """Get menu entry and url/local file for Additional Help.
249
250 User enters a name for the Help resource and a web url or file
251 name. The user can browse for the file.
252 """
253 self.filepath = filepath
254 message = 'Name for item on Help menu:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400255 super().__init__(
256 parent, title, message, text0=menuitem,
257 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400258
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400259 def create_extra(self):
260 "Add path widjets to rows 10-12."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400261 frame = self.frame
262 pathlabel = Label(frame, anchor='w', justify='left',
263 text='Help File Path: Enter URL or browse for file')
264 self.pathvar = StringVar(self, self.filepath)
265 self.path = Entry(frame, textvariable=self.pathvar, width=40)
266 browse = Button(frame, text='Browse', width=8,
267 command=self.browse_file)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400268 self.path_error = Label(frame, text=' ', foreground='red',
269 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400270
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400271 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
272 sticky=W)
273 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
274 pady=[10,0])
275 browse.grid(column=2, row=11, padx=5, sticky=W+S)
276 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
277 sticky=W+E)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400278
279 def askfilename(self, filetypes, initdir, initfile): # htest #
280 # Extracted from browse_file so can mock for unittests.
281 # Cannot unittest as cannot simulate button clicks.
282 # Test by running htest, such as by running this file.
283 return filedialog.Open(parent=self, filetypes=filetypes)\
284 .show(initialdir=initdir, initialfile=initfile)
285
286 def browse_file(self):
287 filetypes = [
288 ("HTML Files", "*.htm *.html", "TEXT"),
289 ("PDF Files", "*.pdf", "TEXT"),
290 ("Windows Help Files", "*.chm"),
291 ("Text Files", "*.txt", "TEXT"),
292 ("All Files", "*")]
293 path = self.pathvar.get()
294 if path:
295 dir, base = os.path.split(path)
296 else:
297 base = None
298 if platform[:3] == 'win':
299 dir = os.path.join(os.path.dirname(executable), 'Doc')
300 if not os.path.isdir(dir):
301 dir = os.getcwd()
302 else:
303 dir = os.getcwd()
304 file = self.askfilename(filetypes, dir, base)
305 if file:
306 self.pathvar.set(file)
307
308 item_ok = SectionName.entry_ok # localize for test override
309
310 def path_ok(self):
311 "Simple validity check for menu file path"
312 path = self.path.get().strip()
313 if not path: #no path specified
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400314 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400315 return None
316 elif not path.startswith(('www.', 'http')):
317 if path[:5] == 'file:':
318 path = path[5:]
319 if not os.path.exists(path):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400320 self.showerror('help file path does not exist.',
321 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400322 return None
323 if platform == 'darwin': # for Mac Safari
324 path = "file://" + path
325 return path
326
327 def entry_ok(self):
328 "Return apparently valid (name, path) or None"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400329 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400330 name = self.item_ok()
331 path = self.path_ok()
332 return None if name is None or path is None else (name, path)
333
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400334class CustomRun(Query):
335 """Get settings for custom run of module.
336
337 1. Command line arguments to extend sys.argv.
338 2. Whether to restart Shell or not.
339 """
340 # Used in runscript.run_custom_event
341
Ngalim Siregar35b87e62019-07-21 22:37:28 +0700342 def __init__(self, parent, title, *, cli_args=[],
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400343 _htest=False, _utest=False):
Ngalim Siregar35b87e62019-07-21 22:37:28 +0700344 """cli_args is a list of strings.
345
346 The list is assigned to the default Entry StringVar.
347 The strings are displayed joined by ' ' for display.
348 """
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400349 message = 'Command Line Arguments for sys.argv:'
350 super().__init__(
351 parent, title, message, text0=cli_args,
352 _htest=_htest, _utest=_utest)
353
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400354 def create_extra(self):
355 "Add run mode on rows 10-12."
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400356 frame = self.frame
357 self.restartvar = BooleanVar(self, value=True)
358 restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
359 offvalue=False, text='Restart shell')
360 self.args_error = Label(frame, text=' ', foreground='red',
361 font=self.error_font)
362
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400363 restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400364 self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
365 sticky='we')
366
367 def cli_args_ok(self):
368 "Validity check and parsing for command line arguments."
369 cli_string = self.entry.get().strip()
370 try:
371 cli_args = shlex.split(cli_string, posix=True)
372 except ValueError as err:
373 self.showerror(str(err))
374 return None
375 return cli_args
376
377 def entry_ok(self):
378 "Return apparently valid (cli_args, restart) or None"
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400379 cli_args = self.cli_args_ok()
380 restart = self.restartvar.get()
381 return None if cli_args is None else (cli_args, restart)
382
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400383
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400384if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400385 from unittest import main
386 main('idlelib.idle_test.test_query', verbosity=2, exit=False)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400387
388 from idlelib.idle_test.htest import run
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400389 run(Query, HelpSource, CustomRun)