blob: 57616de4fe01d4f2811dd76a4a08478cce51bcd5 [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 Reedy8b22c0a2016-07-08 00:22:50 -0400226class HelpSource(Query):
227 "Get menu name and help source for Help menu."
228 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
229
230 def __init__(self, parent, title, *, menuitem='', filepath='',
231 used_names={}, _htest=False, _utest=False):
232 """Get menu entry and url/local file for Additional Help.
233
234 User enters a name for the Help resource and a web url or file
235 name. The user can browse for the file.
236 """
237 self.filepath = filepath
238 message = 'Name for item on Help menu:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400239 super().__init__(
240 parent, title, message, text0=menuitem,
241 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400242
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400243 def create_extra(self):
244 "Add path widjets to rows 10-12."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400245 frame = self.frame
246 pathlabel = Label(frame, anchor='w', justify='left',
247 text='Help File Path: Enter URL or browse for file')
248 self.pathvar = StringVar(self, self.filepath)
249 self.path = Entry(frame, textvariable=self.pathvar, width=40)
250 browse = Button(frame, text='Browse', width=8,
251 command=self.browse_file)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400252 self.path_error = Label(frame, text=' ', foreground='red',
253 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400254
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400255 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
256 sticky=W)
257 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
258 pady=[10,0])
259 browse.grid(column=2, row=11, padx=5, sticky=W+S)
260 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
261 sticky=W+E)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400262
263 def askfilename(self, filetypes, initdir, initfile): # htest #
264 # Extracted from browse_file so can mock for unittests.
265 # Cannot unittest as cannot simulate button clicks.
266 # Test by running htest, such as by running this file.
267 return filedialog.Open(parent=self, filetypes=filetypes)\
268 .show(initialdir=initdir, initialfile=initfile)
269
270 def browse_file(self):
271 filetypes = [
272 ("HTML Files", "*.htm *.html", "TEXT"),
273 ("PDF Files", "*.pdf", "TEXT"),
274 ("Windows Help Files", "*.chm"),
275 ("Text Files", "*.txt", "TEXT"),
276 ("All Files", "*")]
277 path = self.pathvar.get()
278 if path:
279 dir, base = os.path.split(path)
280 else:
281 base = None
282 if platform[:3] == 'win':
283 dir = os.path.join(os.path.dirname(executable), 'Doc')
284 if not os.path.isdir(dir):
285 dir = os.getcwd()
286 else:
287 dir = os.getcwd()
288 file = self.askfilename(filetypes, dir, base)
289 if file:
290 self.pathvar.set(file)
291
292 item_ok = SectionName.entry_ok # localize for test override
293
294 def path_ok(self):
295 "Simple validity check for menu file path"
296 path = self.path.get().strip()
297 if not path: #no path specified
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400298 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400299 return None
300 elif not path.startswith(('www.', 'http')):
301 if path[:5] == 'file:':
302 path = path[5:]
303 if not os.path.exists(path):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400304 self.showerror('help file path does not exist.',
305 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400306 return None
307 if platform == 'darwin': # for Mac Safari
308 path = "file://" + path
309 return path
310
311 def entry_ok(self):
312 "Return apparently valid (name, path) or None"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400313 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400314 name = self.item_ok()
315 path = self.path_ok()
316 return None if name is None or path is None else (name, path)
317
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400318class CustomRun(Query):
319 """Get settings for custom run of module.
320
321 1. Command line arguments to extend sys.argv.
322 2. Whether to restart Shell or not.
323 """
324 # Used in runscript.run_custom_event
325
Ngalim Siregar35b87e62019-07-21 22:37:28 +0700326 def __init__(self, parent, title, *, cli_args=[],
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400327 _htest=False, _utest=False):
Ngalim Siregar35b87e62019-07-21 22:37:28 +0700328 """cli_args is a list of strings.
329
330 The list is assigned to the default Entry StringVar.
331 The strings are displayed joined by ' ' for display.
332 """
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400333 message = 'Command Line Arguments for sys.argv:'
334 super().__init__(
335 parent, title, message, text0=cli_args,
336 _htest=_htest, _utest=_utest)
337
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400338 def create_extra(self):
339 "Add run mode on rows 10-12."
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400340 frame = self.frame
341 self.restartvar = BooleanVar(self, value=True)
342 restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
343 offvalue=False, text='Restart shell')
344 self.args_error = Label(frame, text=' ', foreground='red',
345 font=self.error_font)
346
Terry Jan Reedy54cf2e02019-06-18 17:08:24 -0400347 restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400348 self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
349 sticky='we')
350
351 def cli_args_ok(self):
352 "Validity check and parsing for command line arguments."
353 cli_string = self.entry.get().strip()
354 try:
355 cli_args = shlex.split(cli_string, posix=True)
356 except ValueError as err:
357 self.showerror(str(err))
358 return None
359 return cli_args
360
361 def entry_ok(self):
362 "Return apparently valid (cli_args, restart) or None"
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400363 cli_args = self.cli_args_ok()
364 restart = self.restartvar.get()
365 return None if cli_args is None else (cli_args, restart)
366
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400367
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400368if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400369 from unittest import main
370 main('idlelib.idle_test.test_query', verbosity=2, exit=False)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400371
372 from idlelib.idle_test.htest import run
Cheryl Sabella201bc2d2019-06-17 22:24:10 -0400373 run(Query, HelpSource, CustomRun)