blob: f0b72553db87f7a0abdfaf817bf35ad60f105678 [file] [log] [blame]
Terry Jan Reedy68a53c52016-06-26 22:05:10 -04001"""
2Dialogs that query users and verify the answer before accepting.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -04003
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.
Terry Jan Reedy65db8542016-08-10 12:50:16 -040012Subclass ModuleName gets a name for File => Open Module.
13Subclass HelpSource gets menu item and path for additions to Help menu.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040014"""
15# Query and Section name result from splitting GetCfgSectionNameDialog
16# of configSectionNameDialog.py (temporarily config_sec.py) into
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040017# 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.
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040021
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -040022import importlib
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040023import os
24from sys import executable, platform # Platform is set for one test.
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040025
Victor Stinnerd6debb22017-03-27 16:05:26 +020026from tkinter import Toplevel, StringVar, W, E, S
Terry Jan Reedy68a53c52016-06-26 22:05:10 -040027from tkinter.ttk import Frame, Button, Entry, Label
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040028from tkinter import filedialog
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
Tal Einat10ea9402018-08-02 09:18:29 +0300145 def destroy(self):
146 self.grab_release()
147 super().destroy()
148
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400149
150class SectionName(Query):
151 "Get a name for a config file section name."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400152 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400153
154 def __init__(self, parent, title, message, used_names,
155 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400156 super().__init__(parent, title, message, used_names=used_names,
157 _htest=_htest, _utest=_utest)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400158
159 def entry_ok(self):
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400160 "Return sensible ConfigParser section name or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400161 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400162 name = self.entry.get().strip()
163 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400164 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400165 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400166 elif len(name)>30:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400167 self.showerror('name is longer than 30 characters.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400168 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400169 elif name in self.used_names:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400170 self.showerror('name is already in use.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400171 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400172 return name
173
174
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400175class ModuleName(Query):
176 "Get a module name for Open Module menu entry."
177 # Used in open_module (editor.EditorWindow until move to iobinding).
178
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400179 def __init__(self, parent, title, message, text0,
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400180 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400181 super().__init__(parent, title, message, text0=text0,
182 _htest=_htest, _utest=_utest)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400183
184 def entry_ok(self):
185 "Return entered module name as file path or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400186 self.entry_error['text'] = ''
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400187 name = self.entry.get().strip()
188 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400189 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400190 return None
191 # XXX Ought to insert current file's directory in front of path.
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400192 try:
193 spec = importlib.util.find_spec(name)
194 except (ValueError, ImportError) as msg:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400195 self.showerror(str(msg))
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400196 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400197 if spec is None:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400198 self.showerror("module not found")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400199 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400200 if not isinstance(spec.loader, importlib.abc.SourceLoader):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400201 self.showerror("not a source-based module")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400202 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400203 try:
204 file_path = spec.loader.get_filename(name)
205 except AttributeError:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400206 self.showerror("loader does not support get_filename",
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400207 parent=self)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400208 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400209 return file_path
210
211
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400212class 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:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400225 super().__init__(
226 parent, title, message, text0=menuitem,
227 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400228
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)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400238 self.path_error = Label(frame, text=' ', foreground='red',
239 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400240
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400241 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)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400248
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
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400284 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400285 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):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400290 self.showerror('help file path does not exist.',
291 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400292 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"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400299 self.entry_error['text'] = ''
300 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400301 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
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400306if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400307 from unittest import main
308 main('idlelib.idle_test.test_query', verbosity=2, exit=False)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400309
310 from idlelib.idle_test.htest import run
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400311 run(Query, HelpSource)