• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Dialogs that query users and verify the answer before accepting.
3
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.
12Subclass ModuleName gets a name for File => Open Module.
13Subclass 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
22import importlib.util, importlib.abc
23import os
24import shlex
25from sys import executable, platform  # Platform is set for one test.
26
27from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
28from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
29from tkinter import filedialog
30from tkinter.font import Font
31from tkinter.simpledialog import _setup_dialog
32
33class 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
163class 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
187class 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
229class 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
245class 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
337class 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
387if __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