jpayne@69
|
1 """
|
jpayne@69
|
2 Dialogs that query users and verify the answer before accepting.
|
jpayne@69
|
3
|
jpayne@69
|
4 Query is the generic base class for a popup dialog.
|
jpayne@69
|
5 The user must either enter a valid answer or close the dialog.
|
jpayne@69
|
6 Entries are validated when <Return> is entered or [Ok] is clicked.
|
jpayne@69
|
7 Entries are ignored when [Cancel] or [X] are clicked.
|
jpayne@69
|
8 The 'return value' is .result set to either a valid answer or None.
|
jpayne@69
|
9
|
jpayne@69
|
10 Subclass SectionName gets a name for a new config file section.
|
jpayne@69
|
11 Configdialog uses it for new highlight theme and keybinding set names.
|
jpayne@69
|
12 Subclass ModuleName gets a name for File => Open Module.
|
jpayne@69
|
13 Subclass HelpSource gets menu item and path for additions to Help menu.
|
jpayne@69
|
14 """
|
jpayne@69
|
15 # Query and Section name result from splitting GetCfgSectionNameDialog
|
jpayne@69
|
16 # of configSectionNameDialog.py (temporarily config_sec.py) into
|
jpayne@69
|
17 # generic and specific parts. 3.6 only, July 2016.
|
jpayne@69
|
18 # ModuleName.entry_ok came from editor.EditorWindow.load_module.
|
jpayne@69
|
19 # HelpSource was extracted from configHelpSourceEdit.py (temporarily
|
jpayne@69
|
20 # config_help.py), with darwin code moved from ok to path_ok.
|
jpayne@69
|
21
|
jpayne@69
|
22 import importlib
|
jpayne@69
|
23 import os
|
jpayne@69
|
24 import shlex
|
jpayne@69
|
25 from sys import executable, platform # Platform is set for one test.
|
jpayne@69
|
26
|
jpayne@69
|
27 from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
|
jpayne@69
|
28 from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
|
jpayne@69
|
29 from tkinter import filedialog
|
jpayne@69
|
30 from tkinter.font import Font
|
jpayne@69
|
31
|
jpayne@69
|
32 class Query(Toplevel):
|
jpayne@69
|
33 """Base class for getting verified answer from a user.
|
jpayne@69
|
34
|
jpayne@69
|
35 For this base class, accept any non-blank string.
|
jpayne@69
|
36 """
|
jpayne@69
|
37 def __init__(self, parent, title, message, *, text0='', used_names={},
|
jpayne@69
|
38 _htest=False, _utest=False):
|
jpayne@69
|
39 """Create modal popup, return when destroyed.
|
jpayne@69
|
40
|
jpayne@69
|
41 Additional subclass init must be done before this unless
|
jpayne@69
|
42 _utest=True is passed to suppress wait_window().
|
jpayne@69
|
43
|
jpayne@69
|
44 title - string, title of popup dialog
|
jpayne@69
|
45 message - string, informational message to display
|
jpayne@69
|
46 text0 - initial value for entry
|
jpayne@69
|
47 used_names - names already in use
|
jpayne@69
|
48 _htest - bool, change box location when running htest
|
jpayne@69
|
49 _utest - bool, leave window hidden and not modal
|
jpayne@69
|
50 """
|
jpayne@69
|
51 self.parent = parent # Needed for Font call.
|
jpayne@69
|
52 self.message = message
|
jpayne@69
|
53 self.text0 = text0
|
jpayne@69
|
54 self.used_names = used_names
|
jpayne@69
|
55
|
jpayne@69
|
56 Toplevel.__init__(self, parent)
|
jpayne@69
|
57 self.withdraw() # Hide while configuring, especially geometry.
|
jpayne@69
|
58 self.title(title)
|
jpayne@69
|
59 self.transient(parent)
|
jpayne@69
|
60 self.grab_set()
|
jpayne@69
|
61
|
jpayne@69
|
62 windowingsystem = self.tk.call('tk', 'windowingsystem')
|
jpayne@69
|
63 if windowingsystem == 'aqua':
|
jpayne@69
|
64 try:
|
jpayne@69
|
65 self.tk.call('::tk::unsupported::MacWindowStyle', 'style',
|
jpayne@69
|
66 self._w, 'moveableModal', '')
|
jpayne@69
|
67 except:
|
jpayne@69
|
68 pass
|
jpayne@69
|
69 self.bind("<Command-.>", self.cancel)
|
jpayne@69
|
70 self.bind('<Key-Escape>', self.cancel)
|
jpayne@69
|
71 self.protocol("WM_DELETE_WINDOW", self.cancel)
|
jpayne@69
|
72 self.bind('<Key-Return>', self.ok)
|
jpayne@69
|
73 self.bind("<KP_Enter>", self.ok)
|
jpayne@69
|
74
|
jpayne@69
|
75 self.create_widgets()
|
jpayne@69
|
76 self.update_idletasks() # Need here for winfo_reqwidth below.
|
jpayne@69
|
77 self.geometry( # Center dialog over parent (or below htest box).
|
jpayne@69
|
78 "+%d+%d" % (
|
jpayne@69
|
79 parent.winfo_rootx() +
|
jpayne@69
|
80 (parent.winfo_width()/2 - self.winfo_reqwidth()/2),
|
jpayne@69
|
81 parent.winfo_rooty() +
|
jpayne@69
|
82 ((parent.winfo_height()/2 - self.winfo_reqheight()/2)
|
jpayne@69
|
83 if not _htest else 150)
|
jpayne@69
|
84 ) )
|
jpayne@69
|
85 self.resizable(height=False, width=False)
|
jpayne@69
|
86
|
jpayne@69
|
87 if not _utest:
|
jpayne@69
|
88 self.deiconify() # Unhide now that geometry set.
|
jpayne@69
|
89 self.wait_window()
|
jpayne@69
|
90
|
jpayne@69
|
91 def create_widgets(self, ok_text='OK'): # Do not replace.
|
jpayne@69
|
92 """Create entry (rows, extras, buttons.
|
jpayne@69
|
93
|
jpayne@69
|
94 Entry stuff on rows 0-2, spanning cols 0-2.
|
jpayne@69
|
95 Buttons on row 99, cols 1, 2.
|
jpayne@69
|
96 """
|
jpayne@69
|
97 # Bind to self the widgets needed for entry_ok or unittest.
|
jpayne@69
|
98 self.frame = frame = Frame(self, padding=10)
|
jpayne@69
|
99 frame.grid(column=0, row=0, sticky='news')
|
jpayne@69
|
100 frame.grid_columnconfigure(0, weight=1)
|
jpayne@69
|
101
|
jpayne@69
|
102 entrylabel = Label(frame, anchor='w', justify='left',
|
jpayne@69
|
103 text=self.message)
|
jpayne@69
|
104 self.entryvar = StringVar(self, self.text0)
|
jpayne@69
|
105 self.entry = Entry(frame, width=30, textvariable=self.entryvar)
|
jpayne@69
|
106 self.entry.focus_set()
|
jpayne@69
|
107 self.error_font = Font(name='TkCaptionFont',
|
jpayne@69
|
108 exists=True, root=self.parent)
|
jpayne@69
|
109 self.entry_error = Label(frame, text=' ', foreground='red',
|
jpayne@69
|
110 font=self.error_font)
|
jpayne@69
|
111 entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
|
jpayne@69
|
112 self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
|
jpayne@69
|
113 pady=[10,0])
|
jpayne@69
|
114 self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
|
jpayne@69
|
115 sticky=W+E)
|
jpayne@69
|
116
|
jpayne@69
|
117 self.create_extra()
|
jpayne@69
|
118
|
jpayne@69
|
119 self.button_ok = Button(
|
jpayne@69
|
120 frame, text=ok_text, default='active', command=self.ok)
|
jpayne@69
|
121 self.button_cancel = Button(
|
jpayne@69
|
122 frame, text='Cancel', command=self.cancel)
|
jpayne@69
|
123
|
jpayne@69
|
124 self.button_ok.grid(column=1, row=99, padx=5)
|
jpayne@69
|
125 self.button_cancel.grid(column=2, row=99, padx=5)
|
jpayne@69
|
126
|
jpayne@69
|
127 def create_extra(self): pass # Override to add widgets.
|
jpayne@69
|
128
|
jpayne@69
|
129 def showerror(self, message, widget=None):
|
jpayne@69
|
130 #self.bell(displayof=self)
|
jpayne@69
|
131 (widget or self.entry_error)['text'] = 'ERROR: ' + message
|
jpayne@69
|
132
|
jpayne@69
|
133 def entry_ok(self): # Example: usually replace.
|
jpayne@69
|
134 "Return non-blank entry or None."
|
jpayne@69
|
135 self.entry_error['text'] = ''
|
jpayne@69
|
136 entry = self.entry.get().strip()
|
jpayne@69
|
137 if not entry:
|
jpayne@69
|
138 self.showerror('blank line.')
|
jpayne@69
|
139 return None
|
jpayne@69
|
140 return entry
|
jpayne@69
|
141
|
jpayne@69
|
142 def ok(self, event=None): # Do not replace.
|
jpayne@69
|
143 '''If entry is valid, bind it to 'result' and destroy tk widget.
|
jpayne@69
|
144
|
jpayne@69
|
145 Otherwise leave dialog open for user to correct entry or cancel.
|
jpayne@69
|
146 '''
|
jpayne@69
|
147 entry = self.entry_ok()
|
jpayne@69
|
148 if entry is not None:
|
jpayne@69
|
149 self.result = entry
|
jpayne@69
|
150 self.destroy()
|
jpayne@69
|
151 else:
|
jpayne@69
|
152 # [Ok] moves focus. (<Return> does not.) Move it back.
|
jpayne@69
|
153 self.entry.focus_set()
|
jpayne@69
|
154
|
jpayne@69
|
155 def cancel(self, event=None): # Do not replace.
|
jpayne@69
|
156 "Set dialog result to None and destroy tk widget."
|
jpayne@69
|
157 self.result = None
|
jpayne@69
|
158 self.destroy()
|
jpayne@69
|
159
|
jpayne@69
|
160 def destroy(self):
|
jpayne@69
|
161 self.grab_release()
|
jpayne@69
|
162 super().destroy()
|
jpayne@69
|
163
|
jpayne@69
|
164
|
jpayne@69
|
165 class SectionName(Query):
|
jpayne@69
|
166 "Get a name for a config file section name."
|
jpayne@69
|
167 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
|
jpayne@69
|
168
|
jpayne@69
|
169 def __init__(self, parent, title, message, used_names,
|
jpayne@69
|
170 *, _htest=False, _utest=False):
|
jpayne@69
|
171 super().__init__(parent, title, message, used_names=used_names,
|
jpayne@69
|
172 _htest=_htest, _utest=_utest)
|
jpayne@69
|
173
|
jpayne@69
|
174 def entry_ok(self):
|
jpayne@69
|
175 "Return sensible ConfigParser section name or None."
|
jpayne@69
|
176 self.entry_error['text'] = ''
|
jpayne@69
|
177 name = self.entry.get().strip()
|
jpayne@69
|
178 if not name:
|
jpayne@69
|
179 self.showerror('no name specified.')
|
jpayne@69
|
180 return None
|
jpayne@69
|
181 elif len(name)>30:
|
jpayne@69
|
182 self.showerror('name is longer than 30 characters.')
|
jpayne@69
|
183 return None
|
jpayne@69
|
184 elif name in self.used_names:
|
jpayne@69
|
185 self.showerror('name is already in use.')
|
jpayne@69
|
186 return None
|
jpayne@69
|
187 return name
|
jpayne@69
|
188
|
jpayne@69
|
189
|
jpayne@69
|
190 class ModuleName(Query):
|
jpayne@69
|
191 "Get a module name for Open Module menu entry."
|
jpayne@69
|
192 # Used in open_module (editor.EditorWindow until move to iobinding).
|
jpayne@69
|
193
|
jpayne@69
|
194 def __init__(self, parent, title, message, text0,
|
jpayne@69
|
195 *, _htest=False, _utest=False):
|
jpayne@69
|
196 super().__init__(parent, title, message, text0=text0,
|
jpayne@69
|
197 _htest=_htest, _utest=_utest)
|
jpayne@69
|
198
|
jpayne@69
|
199 def entry_ok(self):
|
jpayne@69
|
200 "Return entered module name as file path or None."
|
jpayne@69
|
201 self.entry_error['text'] = ''
|
jpayne@69
|
202 name = self.entry.get().strip()
|
jpayne@69
|
203 if not name:
|
jpayne@69
|
204 self.showerror('no name specified.')
|
jpayne@69
|
205 return None
|
jpayne@69
|
206 # XXX Ought to insert current file's directory in front of path.
|
jpayne@69
|
207 try:
|
jpayne@69
|
208 spec = importlib.util.find_spec(name)
|
jpayne@69
|
209 except (ValueError, ImportError) as msg:
|
jpayne@69
|
210 self.showerror(str(msg))
|
jpayne@69
|
211 return None
|
jpayne@69
|
212 if spec is None:
|
jpayne@69
|
213 self.showerror("module not found")
|
jpayne@69
|
214 return None
|
jpayne@69
|
215 if not isinstance(spec.loader, importlib.abc.SourceLoader):
|
jpayne@69
|
216 self.showerror("not a source-based module")
|
jpayne@69
|
217 return None
|
jpayne@69
|
218 try:
|
jpayne@69
|
219 file_path = spec.loader.get_filename(name)
|
jpayne@69
|
220 except AttributeError:
|
jpayne@69
|
221 self.showerror("loader does not support get_filename",
|
jpayne@69
|
222 parent=self)
|
jpayne@69
|
223 return None
|
jpayne@69
|
224 return file_path
|
jpayne@69
|
225
|
jpayne@69
|
226
|
jpayne@69
|
227 class HelpSource(Query):
|
jpayne@69
|
228 "Get menu name and help source for Help menu."
|
jpayne@69
|
229 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
|
jpayne@69
|
230
|
jpayne@69
|
231 def __init__(self, parent, title, *, menuitem='', filepath='',
|
jpayne@69
|
232 used_names={}, _htest=False, _utest=False):
|
jpayne@69
|
233 """Get menu entry and url/local file for Additional Help.
|
jpayne@69
|
234
|
jpayne@69
|
235 User enters a name for the Help resource and a web url or file
|
jpayne@69
|
236 name. The user can browse for the file.
|
jpayne@69
|
237 """
|
jpayne@69
|
238 self.filepath = filepath
|
jpayne@69
|
239 message = 'Name for item on Help menu:'
|
jpayne@69
|
240 super().__init__(
|
jpayne@69
|
241 parent, title, message, text0=menuitem,
|
jpayne@69
|
242 used_names=used_names, _htest=_htest, _utest=_utest)
|
jpayne@69
|
243
|
jpayne@69
|
244 def create_extra(self):
|
jpayne@69
|
245 "Add path widjets to rows 10-12."
|
jpayne@69
|
246 frame = self.frame
|
jpayne@69
|
247 pathlabel = Label(frame, anchor='w', justify='left',
|
jpayne@69
|
248 text='Help File Path: Enter URL or browse for file')
|
jpayne@69
|
249 self.pathvar = StringVar(self, self.filepath)
|
jpayne@69
|
250 self.path = Entry(frame, textvariable=self.pathvar, width=40)
|
jpayne@69
|
251 browse = Button(frame, text='Browse', width=8,
|
jpayne@69
|
252 command=self.browse_file)
|
jpayne@69
|
253 self.path_error = Label(frame, text=' ', foreground='red',
|
jpayne@69
|
254 font=self.error_font)
|
jpayne@69
|
255
|
jpayne@69
|
256 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
|
jpayne@69
|
257 sticky=W)
|
jpayne@69
|
258 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
|
jpayne@69
|
259 pady=[10,0])
|
jpayne@69
|
260 browse.grid(column=2, row=11, padx=5, sticky=W+S)
|
jpayne@69
|
261 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
|
jpayne@69
|
262 sticky=W+E)
|
jpayne@69
|
263
|
jpayne@69
|
264 def askfilename(self, filetypes, initdir, initfile): # htest #
|
jpayne@69
|
265 # Extracted from browse_file so can mock for unittests.
|
jpayne@69
|
266 # Cannot unittest as cannot simulate button clicks.
|
jpayne@69
|
267 # Test by running htest, such as by running this file.
|
jpayne@69
|
268 return filedialog.Open(parent=self, filetypes=filetypes)\
|
jpayne@69
|
269 .show(initialdir=initdir, initialfile=initfile)
|
jpayne@69
|
270
|
jpayne@69
|
271 def browse_file(self):
|
jpayne@69
|
272 filetypes = [
|
jpayne@69
|
273 ("HTML Files", "*.htm *.html", "TEXT"),
|
jpayne@69
|
274 ("PDF Files", "*.pdf", "TEXT"),
|
jpayne@69
|
275 ("Windows Help Files", "*.chm"),
|
jpayne@69
|
276 ("Text Files", "*.txt", "TEXT"),
|
jpayne@69
|
277 ("All Files", "*")]
|
jpayne@69
|
278 path = self.pathvar.get()
|
jpayne@69
|
279 if path:
|
jpayne@69
|
280 dir, base = os.path.split(path)
|
jpayne@69
|
281 else:
|
jpayne@69
|
282 base = None
|
jpayne@69
|
283 if platform[:3] == 'win':
|
jpayne@69
|
284 dir = os.path.join(os.path.dirname(executable), 'Doc')
|
jpayne@69
|
285 if not os.path.isdir(dir):
|
jpayne@69
|
286 dir = os.getcwd()
|
jpayne@69
|
287 else:
|
jpayne@69
|
288 dir = os.getcwd()
|
jpayne@69
|
289 file = self.askfilename(filetypes, dir, base)
|
jpayne@69
|
290 if file:
|
jpayne@69
|
291 self.pathvar.set(file)
|
jpayne@69
|
292
|
jpayne@69
|
293 item_ok = SectionName.entry_ok # localize for test override
|
jpayne@69
|
294
|
jpayne@69
|
295 def path_ok(self):
|
jpayne@69
|
296 "Simple validity check for menu file path"
|
jpayne@69
|
297 path = self.path.get().strip()
|
jpayne@69
|
298 if not path: #no path specified
|
jpayne@69
|
299 self.showerror('no help file path specified.', self.path_error)
|
jpayne@69
|
300 return None
|
jpayne@69
|
301 elif not path.startswith(('www.', 'http')):
|
jpayne@69
|
302 if path[:5] == 'file:':
|
jpayne@69
|
303 path = path[5:]
|
jpayne@69
|
304 if not os.path.exists(path):
|
jpayne@69
|
305 self.showerror('help file path does not exist.',
|
jpayne@69
|
306 self.path_error)
|
jpayne@69
|
307 return None
|
jpayne@69
|
308 if platform == 'darwin': # for Mac Safari
|
jpayne@69
|
309 path = "file://" + path
|
jpayne@69
|
310 return path
|
jpayne@69
|
311
|
jpayne@69
|
312 def entry_ok(self):
|
jpayne@69
|
313 "Return apparently valid (name, path) or None"
|
jpayne@69
|
314 self.entry_error['text'] = ''
|
jpayne@69
|
315 self.path_error['text'] = ''
|
jpayne@69
|
316 name = self.item_ok()
|
jpayne@69
|
317 path = self.path_ok()
|
jpayne@69
|
318 return None if name is None or path is None else (name, path)
|
jpayne@69
|
319
|
jpayne@69
|
320 class CustomRun(Query):
|
jpayne@69
|
321 """Get settings for custom run of module.
|
jpayne@69
|
322
|
jpayne@69
|
323 1. Command line arguments to extend sys.argv.
|
jpayne@69
|
324 2. Whether to restart Shell or not.
|
jpayne@69
|
325 """
|
jpayne@69
|
326 # Used in runscript.run_custom_event
|
jpayne@69
|
327
|
jpayne@69
|
328 def __init__(self, parent, title, *, cli_args=[],
|
jpayne@69
|
329 _htest=False, _utest=False):
|
jpayne@69
|
330 """cli_args is a list of strings.
|
jpayne@69
|
331
|
jpayne@69
|
332 The list is assigned to the default Entry StringVar.
|
jpayne@69
|
333 The strings are displayed joined by ' ' for display.
|
jpayne@69
|
334 """
|
jpayne@69
|
335 message = 'Command Line Arguments for sys.argv:'
|
jpayne@69
|
336 super().__init__(
|
jpayne@69
|
337 parent, title, message, text0=cli_args,
|
jpayne@69
|
338 _htest=_htest, _utest=_utest)
|
jpayne@69
|
339
|
jpayne@69
|
340 def create_extra(self):
|
jpayne@69
|
341 "Add run mode on rows 10-12."
|
jpayne@69
|
342 frame = self.frame
|
jpayne@69
|
343 self.restartvar = BooleanVar(self, value=True)
|
jpayne@69
|
344 restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
|
jpayne@69
|
345 offvalue=False, text='Restart shell')
|
jpayne@69
|
346 self.args_error = Label(frame, text=' ', foreground='red',
|
jpayne@69
|
347 font=self.error_font)
|
jpayne@69
|
348
|
jpayne@69
|
349 restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
|
jpayne@69
|
350 self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
|
jpayne@69
|
351 sticky='we')
|
jpayne@69
|
352
|
jpayne@69
|
353 def cli_args_ok(self):
|
jpayne@69
|
354 "Validity check and parsing for command line arguments."
|
jpayne@69
|
355 cli_string = self.entry.get().strip()
|
jpayne@69
|
356 try:
|
jpayne@69
|
357 cli_args = shlex.split(cli_string, posix=True)
|
jpayne@69
|
358 except ValueError as err:
|
jpayne@69
|
359 self.showerror(str(err))
|
jpayne@69
|
360 return None
|
jpayne@69
|
361 return cli_args
|
jpayne@69
|
362
|
jpayne@69
|
363 def entry_ok(self):
|
jpayne@69
|
364 "Return apparently valid (cli_args, restart) or None"
|
jpayne@69
|
365 self.entry_error['text'] = ''
|
jpayne@69
|
366 cli_args = self.cli_args_ok()
|
jpayne@69
|
367 restart = self.restartvar.get()
|
jpayne@69
|
368 return None if cli_args is None else (cli_args, restart)
|
jpayne@69
|
369
|
jpayne@69
|
370
|
jpayne@69
|
371 if __name__ == '__main__':
|
jpayne@69
|
372 from unittest import main
|
jpayne@69
|
373 main('idlelib.idle_test.test_query', verbosity=2, exit=False)
|
jpayne@69
|
374
|
jpayne@69
|
375 from idlelib.idle_test.htest import run
|
jpayne@69
|
376 run(Query, HelpSource, CustomRun)
|