• 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
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
31
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    """
37    def __init__(self, parent, title, message, *, text0='', used_names={},
38                 _htest=False, _utest=False):
39        """Create modal popup, return when destroyed.
40
41        Additional subclass init must be done before this unless
42        _utest=True is passed to suppress wait_window().
43
44        title - string, title of popup dialog
45        message - string, informational message to display
46        text0 - initial value for entry
47        used_names - names already in use
48        _htest - bool, change box location when running htest
49        _utest - bool, leave window hidden and not modal
50        """
51        self.parent = parent  # Needed for Font call.
52        self.message = message
53        self.text0 = text0
54        self.used_names = used_names
55
56        Toplevel.__init__(self, parent)
57        self.withdraw()  # Hide while configuring, especially geometry.
58        self.title(title)
59        self.transient(parent)
60        self.grab_set()
61
62        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)
74
75        self.create_widgets()
76        self.update_idletasks()  # Need here for winfo_reqwidth below.
77        self.geometry(  # Center dialog over parent (or below htest box).
78                "+%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)
84                ) )
85        self.resizable(height=False, width=False)
86
87        if not _utest:
88            self.deiconify()  # Unhide now that geometry set.
89            self.wait_window()
90
91    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.
98        self.frame = frame = Frame(self, padding=10)
99        frame.grid(column=0, row=0, sticky='news')
100        frame.grid_columnconfigure(0, weight=1)
101
102        entrylabel = Label(frame, anchor='w', justify='left',
103                           text=self.message)
104        self.entryvar = StringVar(self, self.text0)
105        self.entry = Entry(frame, width=30, textvariable=self.entryvar)
106        self.entry.focus_set()
107        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)
111        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)
116
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
124        self.button_ok.grid(column=1, row=99, padx=5)
125        self.button_cancel.grid(column=2, row=99, padx=5)
126
127    def create_extra(self): pass  # Override to add widgets.
128
129    def showerror(self, message, widget=None):
130        #self.bell(displayof=self)
131        (widget or self.entry_error)['text'] = 'ERROR: ' + message
132
133    def entry_ok(self):  # Example: usually replace.
134        "Return non-blank entry or None."
135        self.entry_error['text'] = ''
136        entry = self.entry.get().strip()
137        if not entry:
138            self.showerror('blank line.')
139            return None
140        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()
148        if entry is not None:
149            self.result = entry
150            self.destroy()
151        else:
152            # [Ok] moves focus.  (<Return> does not.)  Move it back.
153            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
160    def destroy(self):
161        self.grab_release()
162        super().destroy()
163
164
165class SectionName(Query):
166    "Get a name for a config file section name."
167    # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
168
169    def __init__(self, parent, title, message, used_names,
170                 *, _htest=False, _utest=False):
171        super().__init__(parent, title, message, used_names=used_names,
172                         _htest=_htest, _utest=_utest)
173
174    def entry_ok(self):
175        "Return sensible ConfigParser section name or None."
176        self.entry_error['text'] = ''
177        name = self.entry.get().strip()
178        if not name:
179            self.showerror('no name specified.')
180            return None
181        elif len(name)>30:
182            self.showerror('name is longer than 30 characters.')
183            return None
184        elif name in self.used_names:
185            self.showerror('name is already in use.')
186            return None
187        return name
188
189
190class ModuleName(Query):
191    "Get a module name for Open Module menu entry."
192    # Used in open_module (editor.EditorWindow until move to iobinding).
193
194    def __init__(self, parent, title, message, text0,
195                 *, _htest=False, _utest=False):
196        super().__init__(parent, title, message, text0=text0,
197                       _htest=_htest, _utest=_utest)
198
199    def entry_ok(self):
200        "Return entered module name as file path or None."
201        self.entry_error['text'] = ''
202        name = self.entry.get().strip()
203        if not name:
204            self.showerror('no name specified.')
205            return None
206        # XXX Ought to insert current file's directory in front of path.
207        try:
208            spec = importlib.util.find_spec(name)
209        except (ValueError, ImportError) as msg:
210            self.showerror(str(msg))
211            return None
212        if spec is None:
213            self.showerror("module not found")
214            return None
215        if not isinstance(spec.loader, importlib.abc.SourceLoader):
216            self.showerror("not a source-based module")
217            return None
218        try:
219            file_path = spec.loader.get_filename(name)
220        except AttributeError:
221            self.showerror("loader does not support get_filename",
222                      parent=self)
223            return None
224        return file_path
225
226
227class 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:'
240        super().__init__(
241                parent, title, message, text0=menuitem,
242                used_names=used_names, _htest=_htest, _utest=_utest)
243
244    def create_extra(self):
245        "Add path widjets to rows 10-12."
246        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)
253        self.path_error = Label(frame, text=' ', foreground='red',
254                                font=self.error_font)
255
256        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)
263
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
299            self.showerror('no help file path specified.', self.path_error)
300            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):
305                self.showerror('help file path does not exist.',
306                               self.path_error)
307                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"
314        self.entry_error['text'] = ''
315        self.path_error['text'] = ''
316        name = self.item_ok()
317        path = self.path_ok()
318        return None if name is None or path is None else (name, path)
319
320class 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        """cli_args is a list of strings.
331
332        The list is assigned to the default Entry StringVar.
333        The strings are displayed joined by ' ' for display.
334        """
335        message = 'Command Line Arguments for sys.argv:'
336        super().__init__(
337                parent, title, message, text0=cli_args,
338                _htest=_htest, _utest=_utest)
339
340    def create_extra(self):
341        "Add run mode on rows 10-12."
342        frame = self.frame
343        self.restartvar = BooleanVar(self, value=True)
344        restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
345                              offvalue=False, text='Restart shell')
346        self.args_error = Label(frame, text=' ', foreground='red',
347                                font=self.error_font)
348
349        restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
350        self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
351                             sticky='we')
352
353    def cli_args_ok(self):
354        "Validity check and parsing for command line arguments."
355        cli_string = self.entry.get().strip()
356        try:
357            cli_args = shlex.split(cli_string, posix=True)
358        except ValueError as err:
359            self.showerror(str(err))
360            return None
361        return cli_args
362
363    def entry_ok(self):
364        "Return apparently valid (cli_args, restart) or None"
365        self.entry_error['text'] = ''
366        cli_args = self.cli_args_ok()
367        restart = self.restartvar.get()
368        return None if cli_args is None else (cli_args, restart)
369
370
371if __name__ == '__main__':
372    from unittest import main
373    main('idlelib.idle_test.test_query', verbosity=2, exit=False)
374
375    from idlelib.idle_test.htest import run
376    run(Query, HelpSource, CustomRun)
377