blob: a4584df98d217253051dff4fa80d3eb8162a8050 [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 Reedy65db8542016-08-10 12:50:16 -040026from tkinter import Toplevel, StringVar, W, E, N, S
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040027from tkinter import filedialog
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040028from tkinter.ttk import Frame, Button, Entry, Label
Terry Jan Reedy65db8542016-08-10 12:50:16 -040029from tkinter.font import Font
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040030
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 """
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040036 def __init__(self, parent, title, message, *, text0='', used_names={},
37 _htest=False, _utest=False):
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040038 """Create popup, do not return until tk widget destroyed.
39
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040040 Additional subclass init must be done before calling this
41 unless _utest=True is passed to suppress wait_window().
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040042
43 title - string, title of popup dialog
44 message - string, informational message to display
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040045 text0 - initial value for entry
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040046 used_names - names already in use
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040047 _htest - bool, change box location when running htest
48 _utest - bool, leave window hidden and not modal
49 """
50 Toplevel.__init__(self, parent)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040051 self.withdraw() # Hide while configuring, especially geometry.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040052 self.parent = parent
Terry Jan Reedy65db8542016-08-10 12:50:16 -040053 self.title(title)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040054 self.message = message
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040055 self.text0 = text0
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040056 self.used_names = used_names
Terry Jan Reedy65db8542016-08-10 12:50:16 -040057 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)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040072 self.create_widgets()
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040073 self.update_idletasks() # Needed here for winfo_reqwidth below.
74 self.geometry( # Center dialog over parent (or below htest box).
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040075 "+%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)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040081 ) )
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040082 if not _utest:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040083 self.deiconify() # Unhide now that geometry set.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040084 self.wait_window()
85
86 def create_widgets(self): # Call from override, if any.
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040087 # Bind to self widgets needed for entry_ok or unittest.
Terry Jan Reedy65db8542016-08-10 12:50:16 -040088 self.frame = frame = Frame(self, padding=10)
89 frame.grid(column=0, row=0, sticky='news')
90 frame.grid_columnconfigure(0, weight=1)
91
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040092 entrylabel = Label(frame, anchor='w', justify='left',
93 text=self.message)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040094 self.entryvar = StringVar(self, self.text0)
95 self.entry = Entry(frame, width=30, textvariable=self.entryvar)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040096 self.entry.focus_set()
Terry Jan Reedy65db8542016-08-10 12:50:16 -040097 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)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400105
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400106 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)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400113
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400114 def showerror(self, message, widget=None):
115 #self.bell(displayof=self)
116 (widget or self.entry_error)['text'] = 'ERROR: ' + message
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400117
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400118 def entry_ok(self): # Example: usually replace.
119 "Return non-blank entry or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400120 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400121 entry = self.entry.get().strip()
122 if not entry:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400123 self.showerror('blank line.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400124 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400125 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()
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400133 if entry is not None:
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400134 self.result = entry
135 self.destroy()
136 else:
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400137 # [Ok] moves focus. (<Return> does not.) Move it back.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400138 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
146class SectionName(Query):
147 "Get a name for a config file section name."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400148 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400149
150 def __init__(self, parent, title, message, used_names,
151 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400152 super().__init__(parent, title, message, used_names=used_names,
153 _htest=_htest, _utest=_utest)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400154
155 def entry_ok(self):
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400156 "Return sensible ConfigParser section name or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400157 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400158 name = self.entry.get().strip()
159 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400160 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400161 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400162 elif len(name)>30:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400163 self.showerror('name is longer than 30 characters.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400164 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400165 elif name in self.used_names:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400166 self.showerror('name is already in use.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400167 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400168 return name
169
170
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400171class ModuleName(Query):
172 "Get a module name for Open Module menu entry."
173 # Used in open_module (editor.EditorWindow until move to iobinding).
174
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400175 def __init__(self, parent, title, message, text0,
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400176 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400177 super().__init__(parent, title, message, text0=text0,
178 _htest=_htest, _utest=_utest)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400179
180 def entry_ok(self):
181 "Return entered module name as file path or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400182 self.entry_error['text'] = ''
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400183 name = self.entry.get().strip()
184 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400185 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400186 return None
187 # XXX Ought to insert current file's directory in front of path.
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400188 try:
189 spec = importlib.util.find_spec(name)
190 except (ValueError, ImportError) as msg:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400191 self.showerror(str(msg))
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400192 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400193 if spec is None:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400194 self.showerror("module not found")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400195 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400196 if not isinstance(spec.loader, importlib.abc.SourceLoader):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400197 self.showerror("not a source-based module")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400198 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400199 try:
200 file_path = spec.loader.get_filename(name)
201 except AttributeError:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400202 self.showerror("loader does not support get_filename",
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400203 parent=self)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400204 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400205 return file_path
206
207
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400208class HelpSource(Query):
209 "Get menu name and help source for Help menu."
210 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
211
212 def __init__(self, parent, title, *, menuitem='', filepath='',
213 used_names={}, _htest=False, _utest=False):
214 """Get menu entry and url/local file for Additional Help.
215
216 User enters a name for the Help resource and a web url or file
217 name. The user can browse for the file.
218 """
219 self.filepath = filepath
220 message = 'Name for item on Help menu:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400221 super().__init__(
222 parent, title, message, text0=menuitem,
223 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400224
225 def create_widgets(self):
226 super().create_widgets()
227 frame = self.frame
228 pathlabel = Label(frame, anchor='w', justify='left',
229 text='Help File Path: Enter URL or browse for file')
230 self.pathvar = StringVar(self, self.filepath)
231 self.path = Entry(frame, textvariable=self.pathvar, width=40)
232 browse = Button(frame, text='Browse', width=8,
233 command=self.browse_file)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400234 self.path_error = Label(frame, text=' ', foreground='red',
235 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400236
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400237 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
238 sticky=W)
239 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
240 pady=[10,0])
241 browse.grid(column=2, row=11, padx=5, sticky=W+S)
242 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
243 sticky=W+E)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400244
245 def askfilename(self, filetypes, initdir, initfile): # htest #
246 # Extracted from browse_file so can mock for unittests.
247 # Cannot unittest as cannot simulate button clicks.
248 # Test by running htest, such as by running this file.
249 return filedialog.Open(parent=self, filetypes=filetypes)\
250 .show(initialdir=initdir, initialfile=initfile)
251
252 def browse_file(self):
253 filetypes = [
254 ("HTML Files", "*.htm *.html", "TEXT"),
255 ("PDF Files", "*.pdf", "TEXT"),
256 ("Windows Help Files", "*.chm"),
257 ("Text Files", "*.txt", "TEXT"),
258 ("All Files", "*")]
259 path = self.pathvar.get()
260 if path:
261 dir, base = os.path.split(path)
262 else:
263 base = None
264 if platform[:3] == 'win':
265 dir = os.path.join(os.path.dirname(executable), 'Doc')
266 if not os.path.isdir(dir):
267 dir = os.getcwd()
268 else:
269 dir = os.getcwd()
270 file = self.askfilename(filetypes, dir, base)
271 if file:
272 self.pathvar.set(file)
273
274 item_ok = SectionName.entry_ok # localize for test override
275
276 def path_ok(self):
277 "Simple validity check for menu file path"
278 path = self.path.get().strip()
279 if not path: #no path specified
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400280 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400281 return None
282 elif not path.startswith(('www.', 'http')):
283 if path[:5] == 'file:':
284 path = path[5:]
285 if not os.path.exists(path):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400286 self.showerror('help file path does not exist.',
287 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400288 return None
289 if platform == 'darwin': # for Mac Safari
290 path = "file://" + path
291 return path
292
293 def entry_ok(self):
294 "Return apparently valid (name, path) or None"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400295 self.entry_error['text'] = ''
296 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400297 name = self.item_ok()
298 path = self.path_ok()
299 return None if name is None or path is None else (name, path)
300
301
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400302if __name__ == '__main__':
303 import unittest
304 unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False)
305
306 from idlelib.idle_test.htest import run
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400307 run(Query, HelpSource)