blob: c2628cceb739781a96cd17d61ada0a8f477e1e61 [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
Tal Einat10ea9402018-08-02 09:18:29 +0300146 def destroy(self):
147 self.grab_release()
148 super().destroy()
149
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400150
151class SectionName(Query):
152 "Get a name for a config file section name."
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400153 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400154
155 def __init__(self, parent, title, message, used_names,
156 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400157 super().__init__(parent, title, message, used_names=used_names,
158 _htest=_htest, _utest=_utest)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400159
160 def entry_ok(self):
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400161 "Return sensible ConfigParser section name or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400162 self.entry_error['text'] = ''
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400163 name = self.entry.get().strip()
164 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400165 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400166 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400167 elif len(name)>30:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400168 self.showerror('name is longer than 30 characters.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400169 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400170 elif name in self.used_names:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400171 self.showerror('name is already in use.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400172 return None
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400173 return name
174
175
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400176class ModuleName(Query):
177 "Get a module name for Open Module menu entry."
178 # Used in open_module (editor.EditorWindow until move to iobinding).
179
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400180 def __init__(self, parent, title, message, text0,
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400181 *, _htest=False, _utest=False):
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400182 super().__init__(parent, title, message, text0=text0,
183 _htest=_htest, _utest=_utest)
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400184
185 def entry_ok(self):
186 "Return entered module name as file path or None."
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400187 self.entry_error['text'] = ''
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400188 name = self.entry.get().strip()
189 if not name:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400190 self.showerror('no name specified.')
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400191 return None
192 # XXX Ought to insert current file's directory in front of path.
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400193 try:
194 spec = importlib.util.find_spec(name)
195 except (ValueError, ImportError) as msg:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400196 self.showerror(str(msg))
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400197 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400198 if spec is None:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400199 self.showerror("module not found")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400200 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400201 if not isinstance(spec.loader, importlib.abc.SourceLoader):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400202 self.showerror("not a source-based module")
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400203 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400204 try:
205 file_path = spec.loader.get_filename(name)
206 except AttributeError:
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400207 self.showerror("loader does not support get_filename",
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400208 parent=self)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400209 return None
Terry Jan Reedy0cd6b972016-07-03 19:11:13 -0400210 return file_path
211
212
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400213class HelpSource(Query):
214 "Get menu name and help source for Help menu."
215 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
216
217 def __init__(self, parent, title, *, menuitem='', filepath='',
218 used_names={}, _htest=False, _utest=False):
219 """Get menu entry and url/local file for Additional Help.
220
221 User enters a name for the Help resource and a web url or file
222 name. The user can browse for the file.
223 """
224 self.filepath = filepath
225 message = 'Name for item on Help menu:'
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400226 super().__init__(
227 parent, title, message, text0=menuitem,
228 used_names=used_names, _htest=_htest, _utest=_utest)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400229
230 def create_widgets(self):
231 super().create_widgets()
232 frame = self.frame
233 pathlabel = Label(frame, anchor='w', justify='left',
234 text='Help File Path: Enter URL or browse for file')
235 self.pathvar = StringVar(self, self.filepath)
236 self.path = Entry(frame, textvariable=self.pathvar, width=40)
237 browse = Button(frame, text='Browse', width=8,
238 command=self.browse_file)
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400239 self.path_error = Label(frame, text=' ', foreground='red',
240 font=self.error_font)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400241
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400242 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
243 sticky=W)
244 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
245 pady=[10,0])
246 browse.grid(column=2, row=11, padx=5, sticky=W+S)
247 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
248 sticky=W+E)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400249
250 def askfilename(self, filetypes, initdir, initfile): # htest #
251 # Extracted from browse_file so can mock for unittests.
252 # Cannot unittest as cannot simulate button clicks.
253 # Test by running htest, such as by running this file.
254 return filedialog.Open(parent=self, filetypes=filetypes)\
255 .show(initialdir=initdir, initialfile=initfile)
256
257 def browse_file(self):
258 filetypes = [
259 ("HTML Files", "*.htm *.html", "TEXT"),
260 ("PDF Files", "*.pdf", "TEXT"),
261 ("Windows Help Files", "*.chm"),
262 ("Text Files", "*.txt", "TEXT"),
263 ("All Files", "*")]
264 path = self.pathvar.get()
265 if path:
266 dir, base = os.path.split(path)
267 else:
268 base = None
269 if platform[:3] == 'win':
270 dir = os.path.join(os.path.dirname(executable), 'Doc')
271 if not os.path.isdir(dir):
272 dir = os.getcwd()
273 else:
274 dir = os.getcwd()
275 file = self.askfilename(filetypes, dir, base)
276 if file:
277 self.pathvar.set(file)
278
279 item_ok = SectionName.entry_ok # localize for test override
280
281 def path_ok(self):
282 "Simple validity check for menu file path"
283 path = self.path.get().strip()
284 if not path: #no path specified
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400285 self.showerror('no help file path specified.', self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400286 return None
287 elif not path.startswith(('www.', 'http')):
288 if path[:5] == 'file:':
289 path = path[5:]
290 if not os.path.exists(path):
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400291 self.showerror('help file path does not exist.',
292 self.path_error)
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400293 return None
294 if platform == 'darwin': # for Mac Safari
295 path = "file://" + path
296 return path
297
298 def entry_ok(self):
299 "Return apparently valid (name, path) or None"
Terry Jan Reedy65db8542016-08-10 12:50:16 -0400300 self.entry_error['text'] = ''
301 self.path_error['text'] = ''
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400302 name = self.item_ok()
303 path = self.path_ok()
304 return None if name is None or path is None else (name, path)
305
306
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400307if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400308 from unittest import main
309 main('idlelib.idle_test.test_query', verbosity=2, exit=False)
Terry Jan Reedy68a53c52016-06-26 22:05:10 -0400310
311 from idlelib.idle_test.htest import run
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -0400312 run(Query, HelpSource)