• 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
24from sys import executable, platform  # Platform is set for one test.
25
26from tkinter import Toplevel, StringVar, W, E, S
27from tkinter.ttk import Frame, Button, Entry, Label
28from tkinter import filedialog
29from tkinter.font import Font
30
31class Query(Toplevel):
32    """Base class for getting verified answer from a user.
33
34    For this base class, accept any non-blank string.
35    """
36    def __init__(self, parent, title, message, *, text0='', used_names={},
37                 _htest=False, _utest=False):
38        """Create popup, do not return until tk widget destroyed.
39
40        Additional subclass init must be done before calling this
41        unless  _utest=True is passed to suppress wait_window().
42
43        title - string, title of popup dialog
44        message - string, informational message to display
45        text0 - initial value for entry
46        used_names - names already in use
47        _htest - bool, change box location when running htest
48        _utest - bool, leave window hidden and not modal
49        """
50        Toplevel.__init__(self, parent)
51        self.withdraw()  # Hide while configuring, especially geometry.
52        self.parent = parent
53        self.title(title)
54        self.message = message
55        self.text0 = text0
56        self.used_names = used_names
57        self.transient(parent)
58        self.grab_set()
59        windowingsystem = self.tk.call('tk', 'windowingsystem')
60        if windowingsystem == 'aqua':
61            try:
62                self.tk.call('::tk::unsupported::MacWindowStyle', 'style',
63                             self._w, 'moveableModal', '')
64            except:
65                pass
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        self.resizable(height=False, width=False)
72        self.create_widgets()
73        self.update_idletasks()  # Needed 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        if not _utest:
83            self.deiconify()  # Unhide now that geometry set.
84            self.wait_window()
85
86    def create_widgets(self):  # Call from override, if any.
87        # Bind to self widgets needed for entry_ok or unittest.
88        self.frame = frame = Frame(self, padding=10)
89        frame.grid(column=0, row=0, sticky='news')
90        frame.grid_columnconfigure(0, weight=1)
91
92        entrylabel = Label(frame, anchor='w', justify='left',
93                           text=self.message)
94        self.entryvar = StringVar(self, self.text0)
95        self.entry = Entry(frame, width=30, textvariable=self.entryvar)
96        self.entry.focus_set()
97        self.error_font = Font(name='TkCaptionFont',
98                               exists=True, root=self.parent)
99        self.entry_error = Label(frame, text=' ', foreground='red',
100                                 font=self.error_font)
101        self.button_ok = Button(
102                frame, text='OK', default='active', command=self.ok)
103        self.button_cancel = Button(
104                frame, text='Cancel', command=self.cancel)
105
106        entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
107        self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
108                        pady=[10,0])
109        self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
110                              sticky=W+E)
111        self.button_ok.grid(column=1, row=99, padx=5)
112        self.button_cancel.grid(column=2, row=99, padx=5)
113
114    def showerror(self, message, widget=None):
115        #self.bell(displayof=self)
116        (widget or self.entry_error)['text'] = 'ERROR: ' + message
117
118    def entry_ok(self):  # Example: usually replace.
119        "Return non-blank entry or None."
120        self.entry_error['text'] = ''
121        entry = self.entry.get().strip()
122        if not entry:
123            self.showerror('blank line.')
124            return None
125        return entry
126
127    def ok(self, event=None):  # Do not replace.
128        '''If entry is valid, bind it to 'result' and destroy tk widget.
129
130        Otherwise leave dialog open for user to correct entry or cancel.
131        '''
132        entry = self.entry_ok()
133        if entry is not None:
134            self.result = entry
135            self.destroy()
136        else:
137            # [Ok] moves focus.  (<Return> does not.)  Move it back.
138            self.entry.focus_set()
139
140    def cancel(self, event=None):  # Do not replace.
141        "Set dialog result to None and destroy tk widget."
142        self.result = None
143        self.destroy()
144
145    def destroy(self):
146        self.grab_release()
147        super().destroy()
148
149
150class SectionName(Query):
151    "Get a name for a config file section name."
152    # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
153
154    def __init__(self, parent, title, message, used_names,
155                 *, _htest=False, _utest=False):
156        super().__init__(parent, title, message, used_names=used_names,
157                         _htest=_htest, _utest=_utest)
158
159    def entry_ok(self):
160        "Return sensible ConfigParser section name or None."
161        self.entry_error['text'] = ''
162        name = self.entry.get().strip()
163        if not name:
164            self.showerror('no name specified.')
165            return None
166        elif len(name)>30:
167            self.showerror('name is longer than 30 characters.')
168            return None
169        elif name in self.used_names:
170            self.showerror('name is already in use.')
171            return None
172        return name
173
174
175class ModuleName(Query):
176    "Get a module name for Open Module menu entry."
177    # Used in open_module (editor.EditorWindow until move to iobinding).
178
179    def __init__(self, parent, title, message, text0,
180                 *, _htest=False, _utest=False):
181        super().__init__(parent, title, message, text0=text0,
182                       _htest=_htest, _utest=_utest)
183
184    def entry_ok(self):
185        "Return entered module name as file path or None."
186        self.entry_error['text'] = ''
187        name = self.entry.get().strip()
188        if not name:
189            self.showerror('no name specified.')
190            return None
191        # XXX Ought to insert current file's directory in front of path.
192        try:
193            spec = importlib.util.find_spec(name)
194        except (ValueError, ImportError) as msg:
195            self.showerror(str(msg))
196            return None
197        if spec is None:
198            self.showerror("module not found")
199            return None
200        if not isinstance(spec.loader, importlib.abc.SourceLoader):
201            self.showerror("not a source-based module")
202            return None
203        try:
204            file_path = spec.loader.get_filename(name)
205        except AttributeError:
206            self.showerror("loader does not support get_filename",
207                      parent=self)
208            return None
209        return file_path
210
211
212class HelpSource(Query):
213    "Get menu name and help source for Help menu."
214    # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
215
216    def __init__(self, parent, title, *, menuitem='', filepath='',
217                 used_names={}, _htest=False, _utest=False):
218        """Get menu entry and url/local file for Additional Help.
219
220        User enters a name for the Help resource and a web url or file
221        name. The user can browse for the file.
222        """
223        self.filepath = filepath
224        message = 'Name for item on Help menu:'
225        super().__init__(
226                parent, title, message, text0=menuitem,
227                used_names=used_names, _htest=_htest, _utest=_utest)
228
229    def create_widgets(self):
230        super().create_widgets()
231        frame = self.frame
232        pathlabel = Label(frame, anchor='w', justify='left',
233                          text='Help File Path: Enter URL or browse for file')
234        self.pathvar = StringVar(self, self.filepath)
235        self.path = Entry(frame, textvariable=self.pathvar, width=40)
236        browse = Button(frame, text='Browse', width=8,
237                        command=self.browse_file)
238        self.path_error = Label(frame, text=' ', foreground='red',
239                                font=self.error_font)
240
241        pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
242                       sticky=W)
243        self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
244                       pady=[10,0])
245        browse.grid(column=2, row=11, padx=5, sticky=W+S)
246        self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
247                             sticky=W+E)
248
249    def askfilename(self, filetypes, initdir, initfile):  # htest #
250        # Extracted from browse_file so can mock for unittests.
251        # Cannot unittest as cannot simulate button clicks.
252        # Test by running htest, such as by running this file.
253        return filedialog.Open(parent=self, filetypes=filetypes)\
254               .show(initialdir=initdir, initialfile=initfile)
255
256    def browse_file(self):
257        filetypes = [
258            ("HTML Files", "*.htm *.html", "TEXT"),
259            ("PDF Files", "*.pdf", "TEXT"),
260            ("Windows Help Files", "*.chm"),
261            ("Text Files", "*.txt", "TEXT"),
262            ("All Files", "*")]
263        path = self.pathvar.get()
264        if path:
265            dir, base = os.path.split(path)
266        else:
267            base = None
268            if platform[:3] == 'win':
269                dir = os.path.join(os.path.dirname(executable), 'Doc')
270                if not os.path.isdir(dir):
271                    dir = os.getcwd()
272            else:
273                dir = os.getcwd()
274        file = self.askfilename(filetypes, dir, base)
275        if file:
276            self.pathvar.set(file)
277
278    item_ok = SectionName.entry_ok  # localize for test override
279
280    def path_ok(self):
281        "Simple validity check for menu file path"
282        path = self.path.get().strip()
283        if not path: #no path specified
284            self.showerror('no help file path specified.', self.path_error)
285            return None
286        elif not path.startswith(('www.', 'http')):
287            if path[:5] == 'file:':
288                path = path[5:]
289            if not os.path.exists(path):
290                self.showerror('help file path does not exist.',
291                               self.path_error)
292                return None
293            if platform == 'darwin':  # for Mac Safari
294                path =  "file://" + path
295        return path
296
297    def entry_ok(self):
298        "Return apparently valid (name, path) or None"
299        self.entry_error['text'] = ''
300        self.path_error['text'] = ''
301        name = self.item_ok()
302        path = self.path_ok()
303        return None if name is None or path is None else (name, path)
304
305
306if __name__ == '__main__':
307    from unittest import main
308    main('idlelib.idle_test.test_query', verbosity=2, exit=False)
309
310    from idlelib.idle_test.htest import run
311    run(Query, HelpSource)
312