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