blob: 593506383c41abcfd185309bfcfa8816d3b9c5c8 [file] [log] [blame]
Terry Jan Reedy68a53c52016-06-26 22:05:10 -04001"""
2Dialogs that query users and verify the answer before accepting.
3Use ttk widgets, limiting use to tcl/tk 8.5+, as in IDLE 3.6+.
4
5Query is the generic base class for a popup dialog.
6The user must either enter a valid answer or close the dialog.
7Entries are validated when <Return> is entered or [Ok] is clicked.
8Entries are ignored when [Cancel] or [X] are clicked.
9The 'return value' is .result set to either a valid answer or None.
10
11Subclass SectionName gets a name for a new config file section.
12Configdialog uses it for new highlight theme and keybinding set names.
Terry Jan Reedy65db8542016-08-10 12:50:16 -040013Subclass ModuleName gets a name for File => Open Module.
14Subclass HelpSource gets menu item and path for additions to Help menu.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040015"""
16# Query and Section name result from splitting GetCfgSectionNameDialog
17# of configSectionNameDialog.py (temporarily config_sec.py) into
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040018# generic and specific parts. 3.6 only, July 2016.
19# ModuleName.entry_ok came from editor.EditorWindow.load_module.
20# HelpSource was extracted from configHelpSourceEdit.py (temporarily
21# config_help.py), with darwin code moved from ok to path_ok.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040022
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040023import importlib
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040024import os
25from sys import executable, platform # Platform is set for one test.
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040026
Victor Stinnerd6debb22017-03-27 16:05:26 +020027from tkinter import Toplevel, StringVar, W, E, S
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040028from tkinter.ttk import Frame, Button, Entry, Label
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040029from tkinter import filedialog
Terry Jan Reedy65db8542016-08-10 12:50:16 -040030from tkinter.font import Font
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040031
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 """
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040037 def __init__(self, parent, title, message, *, text0='', used_names={},
38 _htest=False, _utest=False):
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040039 """Create popup, do not return until tk widget destroyed.
40
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040041 Additional subclass init must be done before calling this
42 unless _utest=True is passed to suppress wait_window().
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040043
44 title - string, title of popup dialog
45 message - string, informational message to display
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040046 text0 - initial value for entry
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040047 used_names - names already in use
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040048 _htest - bool, change box location when running htest
49 _utest - bool, leave window hidden and not modal
50 """
51 Toplevel.__init__(self, parent)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040052 self.withdraw() # Hide while configuring, especially geometry.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040053 self.parent = parent
Terry Jan Reedy65db8542016-08-10 12:50:16 -040054 self.title(title)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040055 self.message = message
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040056 self.text0 = text0
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040057 self.used_names = used_names
Terry Jan Reedy65db8542016-08-10 12:50:16 -040058 self.transient(parent)
59 self.grab_set()
60 windowingsystem = self.tk.call('tk', 'windowingsystem')
61 if windowingsystem == 'aqua':
62 try:
63 self.tk.call('::tk::unsupported::MacWindowStyle', 'style',
64 self._w, 'moveableModal', '')
65 except:
66 pass
67 self.bind("<Command-.>", self.cancel)
68 self.bind('<Key-Escape>', self.cancel)
69 self.protocol("WM_DELETE_WINDOW", self.cancel)
70 self.bind('<Key-Return>', self.ok)
71 self.bind("<KP_Enter>", self.ok)
72 self.resizable(height=False, width=False)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040073 self.create_widgets()
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040074 self.update_idletasks() # Needed here for winfo_reqwidth below.
75 self.geometry( # Center dialog over parent (or below htest box).
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040076 "+%d+%d" % (
77 parent.winfo_rootx() +
78 (parent.winfo_width()/2 - self.winfo_reqwidth()/2),
79 parent.winfo_rooty() +
80 ((parent.winfo_height()/2 - self.winfo_reqheight()/2)
81 if not _htest else 150)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040082 ) )
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040083 if not _utest:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040084 self.deiconify() # Unhide now that geometry set.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040085 self.wait_window()
86
87 def create_widgets(self): # Call from override, if any.
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040088 # Bind to self widgets needed for entry_ok or unittest.
Terry Jan Reedy65db8542016-08-10 12:50:16 -040089 self.frame = frame = Frame(self, padding=10)
90 frame.grid(column=0, row=0, sticky='news')
91 frame.grid_columnconfigure(0, weight=1)
92
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040093 entrylabel = Label(frame, anchor='w', justify='left',
94 text=self.message)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040095 self.entryvar = StringVar(self, self.text0)
96 self.entry = Entry(frame, width=30, textvariable=self.entryvar)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040097 self.entry.focus_set()
Terry Jan Reedy65db8542016-08-10 12:50:16 -040098 self.error_font = Font(name='TkCaptionFont',
99 exists=True, root=self.parent)
100 self.entry_error = Label(frame, text=' ', foreground='red',
101 font=self.error_font)
102 self.button_ok = Button(
103 frame, text='OK', default='active', command=self.ok)
104 self.button_cancel = Button(
105 frame, text='Cancel', command=self.cancel)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400106
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400107 entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
108 self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
109 pady=[10,0])
110 self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
111 sticky=W+E)
112 self.button_ok.grid(column=1, row=99, padx=5)
113 self.button_cancel.grid(column=2, row=99, padx=5)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400114
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400115 def showerror(self, message, widget=None):
116 #self.bell(displayof=self)
117 (widget or self.entry_error)['text'] = 'ERROR: ' + message
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400118
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400119 def entry_ok(self): # Example: usually replace.
120 "Return non-blank entry or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400121 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400122 entry = self.entry.get().strip()
123 if not entry:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400124 self.showerror('blank line.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400125 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400126 return entry
127
128 def ok(self, event=None): # Do not replace.
129 '''If entry is valid, bind it to 'result' and destroy tk widget.
130
131 Otherwise leave dialog open for user to correct entry or cancel.
132 '''
133 entry = self.entry_ok()
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400134 if entry is not None:
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400135 self.result = entry
136 self.destroy()
137 else:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400138 # [Ok] moves focus. (<Return> does not.) Move it back.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400139 self.entry.focus_set()
140
141 def cancel(self, event=None): # Do not replace.
142 "Set dialog result to None and destroy tk widget."
143 self.result = None
144 self.destroy()
145
146
147class SectionName(Query):
148 "Get a name for a config file section name."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400149 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400150
151 def __init__(self, parent, title, message, used_names,
152 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400153 super().__init__(parent, title, message, used_names=used_names,
154 _htest=_htest, _utest=_utest)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400155
156 def entry_ok(self):
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400157 "Return sensible ConfigParser section name or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400158 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400159 name = self.entry.get().strip()
160 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400161 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400162 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400163 elif len(name)>30:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400164 self.showerror('name is longer than 30 characters.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400165 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400166 elif name in self.used_names:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400167 self.showerror('name is already in use.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400168 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400169 return name
170
171
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400172class ModuleName(Query):
173 "Get a module name for Open Module menu entry."
174 # Used in open_module (editor.EditorWindow until move to iobinding).
175
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400176 def __init__(self, parent, title, message, text0,
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400177 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400178 super().__init__(parent, title, message, text0=text0,
179 _htest=_htest, _utest=_utest)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400180
181 def entry_ok(self):
182 "Return entered module name as file path or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400183 self.entry_error['text'] = ''
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400184 name = self.entry.get().strip()
185 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400186 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400187 return None
188 # XXX Ought to insert current file's directory in front of path.
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400189 try:
190 spec = importlib.util.find_spec(name)
191 except (ValueError, ImportError) as msg:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400192 self.showerror(str(msg))
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400193 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400194 if spec is None:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400195 self.showerror("module not found")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400196 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400197 if not isinstance(spec.loader, importlib.abc.SourceLoader):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400198 self.showerror("not a source-based module")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400199 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400200 try:
201 file_path = spec.loader.get_filename(name)
202 except AttributeError:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400203 self.showerror("loader does not support get_filename",
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400204 parent=self)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400205 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400206 return file_path
207
208
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400209class HelpSource(Query):
210 "Get menu name and help source for Help menu."
211 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
212
213 def __init__(self, parent, title, *, menuitem='', filepath='',
214 used_names={}, _htest=False, _utest=False):
215 """Get menu entry and url/local file for Additional Help.
216
217 User enters a name for the Help resource and a web url or file
218 name. The user can browse for the file.
219 """
220 self.filepath = filepath
221 message = 'Name for item on Help menu:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400222 super().__init__(
223 parent, title, message, text0=menuitem,
224 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400225
226 def create_widgets(self):
227 super().create_widgets()
228 frame = self.frame
229 pathlabel = Label(frame, anchor='w', justify='left',
230 text='Help File Path: Enter URL or browse for file')
231 self.pathvar = StringVar(self, self.filepath)
232 self.path = Entry(frame, textvariable=self.pathvar, width=40)
233 browse = Button(frame, text='Browse', width=8,
234 command=self.browse_file)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400235 self.path_error = Label(frame, text=' ', foreground='red',
236 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400237
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400238 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
239 sticky=W)
240 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
241 pady=[10,0])
242 browse.grid(column=2, row=11, padx=5, sticky=W+S)
243 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
244 sticky=W+E)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400245
246 def askfilename(self, filetypes, initdir, initfile): # htest #
247 # Extracted from browse_file so can mock for unittests.
248 # Cannot unittest as cannot simulate button clicks.
249 # Test by running htest, such as by running this file.
250 return filedialog.Open(parent=self, filetypes=filetypes)\
251 .show(initialdir=initdir, initialfile=initfile)
252
253 def browse_file(self):
254 filetypes = [
255 ("HTML Files", "*.htm *.html", "TEXT"),
256 ("PDF Files", "*.pdf", "TEXT"),
257 ("Windows Help Files", "*.chm"),
258 ("Text Files", "*.txt", "TEXT"),
259 ("All Files", "*")]
260 path = self.pathvar.get()
261 if path:
262 dir, base = os.path.split(path)
263 else:
264 base = None
265 if platform[:3] == 'win':
266 dir = os.path.join(os.path.dirname(executable), 'Doc')
267 if not os.path.isdir(dir):
268 dir = os.getcwd()
269 else:
270 dir = os.getcwd()
271 file = self.askfilename(filetypes, dir, base)
272 if file:
273 self.pathvar.set(file)
274
275 item_ok = SectionName.entry_ok # localize for test override
276
277 def path_ok(self):
278 "Simple validity check for menu file path"
279 path = self.path.get().strip()
280 if not path: #no path specified
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400281 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400282 return None
283 elif not path.startswith(('www.', 'http')):
284 if path[:5] == 'file:':
285 path = path[5:]
286 if not os.path.exists(path):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400287 self.showerror('help file path does not exist.',
288 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400289 return None
290 if platform == 'darwin': # for Mac Safari
291 path = "file://" + path
292 return path
293
294 def entry_ok(self):
295 "Return apparently valid (name, path) or None"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400296 self.entry_error['text'] = ''
297 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400298 name = self.item_ok()
299 path = self.path_ok()
300 return None if name is None or path is None else (name, path)
301
302
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400303if __name__ == '__main__':
304 import unittest
305 unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False)
306
307 from idlelib.idle_test.htest import run
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400308 run(Query, HelpSource)