blob: d74084feed7655cba16dd2fe2cc3159014bccdf1 [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 Reedy65db8542016-08-10 12:50:16 -0400111 entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
112 self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
113 pady=[10,0])
114 self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
115 sticky=W+E)
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400116
117 self.create_extra()
118
119 self.button_ok = Button(
120 frame, text=ok_text, default='active', command=self.ok)
121 self.button_cancel = Button(
122 frame, text='Cancel', command=self.cancel)
123
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400124 self.button_ok.grid(column=1, row=99, padx=5)
125 self.button_cancel.grid(column=2, row=99, padx=5)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400126
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400127 def create_extra(self): pass # Override to add widgets.
128
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400129 def showerror(self, message, widget=None):
130 #self.bell(displayof=self)
131 (widget or self.entry_error)['text'] = 'ERROR: ' + message
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400132
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400133 def entry_ok(self): # Example: usually replace.
134 "Return non-blank entry or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400135 self.entry_error['text'] = ''
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 '''
147 entry = self.entry_ok()
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400148 if entry is not None:
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400149 self.result = entry
150 self.destroy()
151 else:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400152 # [Ok] moves focus. (<Return> does not.) Move it back.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400153 self.entry.focus_set()
154
155 def cancel(self, event=None): # Do not replace.
156 "Set dialog result to None and destroy tk widget."
157 self.result = None
158 self.destroy()
159
Tal Einat10ea9402018-08-02 09:18:29 +0300160 def destroy(self):
161 self.grab_release()
162 super().destroy()
163
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400164
165class SectionName(Query):
166 "Get a name for a config file section name."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400167 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400168
169 def __init__(self, parent, title, message, used_names,
170 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400171 super().__init__(parent, title, message, used_names=used_names,
172 _htest=_htest, _utest=_utest)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400173
174 def entry_ok(self):
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400175 "Return sensible ConfigParser section name or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400176 self.entry_error['text'] = ''
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 Reedy65db8542016-08-10 12:50:16 -0400201 self.entry_error['text'] = ''
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:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400213 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):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400216 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:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400221 self.showerror("loader does not support get_filename",
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400222 parent=self)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400223 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400224 return file_path
225
226
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400227class HelpSource(Query):
228 "Get menu name and help source for Help menu."
229 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
230
231 def __init__(self, parent, title, *, menuitem='', filepath='',
232 used_names={}, _htest=False, _utest=False):
233 """Get menu entry and url/local file for Additional Help.
234
235 User enters a name for the Help resource and a web url or file
236 name. The user can browse for the file.
237 """
238 self.filepath = filepath
239 message = 'Name for item on Help menu:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400240 super().__init__(
241 parent, title, message, text0=menuitem,
242 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400243
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400244 def create_extra(self):
245 "Add path widjets to rows 10-12."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400246 frame = self.frame
247 pathlabel = Label(frame, anchor='w', justify='left',
248 text='Help File Path: Enter URL or browse for file')
249 self.pathvar = StringVar(self, self.filepath)
250 self.path = Entry(frame, textvariable=self.pathvar, width=40)
251 browse = Button(frame, text='Browse', width=8,
252 command=self.browse_file)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400253 self.path_error = Label(frame, text=' ', foreground='red',
254 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400255
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400256 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
257 sticky=W)
258 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
259 pady=[10,0])
260 browse.grid(column=2, row=11, padx=5, sticky=W+S)
261 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
262 sticky=W+E)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400263
264 def askfilename(self, filetypes, initdir, initfile): # htest #
265 # Extracted from browse_file so can mock for unittests.
266 # Cannot unittest as cannot simulate button clicks.
267 # Test by running htest, such as by running this file.
268 return filedialog.Open(parent=self, filetypes=filetypes)\
269 .show(initialdir=initdir, initialfile=initfile)
270
271 def browse_file(self):
272 filetypes = [
273 ("HTML Files", "*.htm *.html", "TEXT"),
274 ("PDF Files", "*.pdf", "TEXT"),
275 ("Windows Help Files", "*.chm"),
276 ("Text Files", "*.txt", "TEXT"),
277 ("All Files", "*")]
278 path = self.pathvar.get()
279 if path:
280 dir, base = os.path.split(path)
281 else:
282 base = None
283 if platform[:3] == 'win':
284 dir = os.path.join(os.path.dirname(executable), 'Doc')
285 if not os.path.isdir(dir):
286 dir = os.getcwd()
287 else:
288 dir = os.getcwd()
289 file = self.askfilename(filetypes, dir, base)
290 if file:
291 self.pathvar.set(file)
292
293 item_ok = SectionName.entry_ok # localize for test override
294
295 def path_ok(self):
296 "Simple validity check for menu file path"
297 path = self.path.get().strip()
298 if not path: #no path specified
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400299 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400300 return None
301 elif not path.startswith(('www.', 'http')):
302 if path[:5] == 'file:':
303 path = path[5:]
304 if not os.path.exists(path):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400305 self.showerror('help file path does not exist.',
306 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400307 return None
308 if platform == 'darwin': # for Mac Safari
309 path = "file://" + path
310 return path
311
312 def entry_ok(self):
313 "Return apparently valid (name, path) or None"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400314 self.entry_error['text'] = ''
315 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400316 name = self.item_ok()
317 path = self.path_ok()
318 return None if name is None or path is None else (name, path)
319
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400320class CustomRun(Query):
321 """Get settings for custom run of module.
322
323 1. Command line arguments to extend sys.argv.
324 2. Whether to restart Shell or not.
325 """
326 # Used in runscript.run_custom_event
327
328 def __init__(self, parent, title, *, cli_args='',
329 _htest=False, _utest=False):
330 # TODO Use cli_args to pre-populate entry.
331 message = 'Command Line Arguments for sys.argv:'
332 super().__init__(
333 parent, title, message, text0=cli_args,
334 _htest=_htest, _utest=_utest)
335
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400336 def create_extra(self):
337 "Add run mode on rows 10-12."
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400338 frame = self.frame
339 self.restartvar = BooleanVar(self, value=True)
340 restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
341 offvalue=False, text='Restart shell')
342 self.args_error = Label(frame, text=' ', foreground='red',
343 font=self.error_font)
344
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400345 restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400346 self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
347 sticky='we')
348
349 def cli_args_ok(self):
350 "Validity check and parsing for command line arguments."
351 cli_string = self.entry.get().strip()
352 try:
353 cli_args = shlex.split(cli_string, posix=True)
354 except ValueError as err:
355 self.showerror(str(err))
356 return None
357 return cli_args
358
359 def entry_ok(self):
360 "Return apparently valid (cli_args, restart) or None"
361 self.entry_error['text'] = ''
362 cli_args = self.cli_args_ok()
363 restart = self.restartvar.get()
364 return None if cli_args is None else (cli_args, restart)
365
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400366
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400367if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400368 from unittest import main
369 main('idlelib.idle_test.test_query', verbosity=2, exit=False)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400370
371 from idlelib.idle_test.htest import run
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400372 run(Query, HelpSource, CustomRun)