Mercurial > repos > rliterman > csp2
comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/query.py @ 68:5028fdace37b
planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author | jpayne |
---|---|
date | Tue, 18 Mar 2025 16:23:26 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
67:0e9998148a16 | 68:5028fdace37b |
---|---|
1 """ | |
2 Dialogs that query users and verify the answer before accepting. | |
3 | |
4 Query is the generic base class for a popup dialog. | |
5 The user must either enter a valid answer or close the dialog. | |
6 Entries are validated when <Return> is entered or [Ok] is clicked. | |
7 Entries are ignored when [Cancel] or [X] are clicked. | |
8 The 'return value' is .result set to either a valid answer or None. | |
9 | |
10 Subclass SectionName gets a name for a new config file section. | |
11 Configdialog uses it for new highlight theme and keybinding set names. | |
12 Subclass ModuleName gets a name for File => Open Module. | |
13 Subclass 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 | |
22 import importlib | |
23 import os | |
24 import shlex | |
25 from sys import executable, platform # Platform is set for one test. | |
26 | |
27 from tkinter import Toplevel, StringVar, BooleanVar, W, E, S | |
28 from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton | |
29 from tkinter import filedialog | |
30 from tkinter.font import Font | |
31 | |
32 class Query(Toplevel): | |
33 """Base class for getting verified answer from a user. | |
34 | |
35 For this base class, accept any non-blank string. | |
36 """ | |
37 def __init__(self, parent, title, message, *, text0='', used_names={}, | |
38 _htest=False, _utest=False): | |
39 """Create modal popup, return when destroyed. | |
40 | |
41 Additional subclass init must be done before this unless | |
42 _utest=True is passed to suppress wait_window(). | |
43 | |
44 title - string, title of popup dialog | |
45 message - string, informational message to display | |
46 text0 - initial value for entry | |
47 used_names - names already in use | |
48 _htest - bool, change box location when running htest | |
49 _utest - bool, leave window hidden and not modal | |
50 """ | |
51 self.parent = parent # Needed for Font call. | |
52 self.message = message | |
53 self.text0 = text0 | |
54 self.used_names = used_names | |
55 | |
56 Toplevel.__init__(self, parent) | |
57 self.withdraw() # Hide while configuring, especially geometry. | |
58 self.title(title) | |
59 self.transient(parent) | |
60 self.grab_set() | |
61 | |
62 windowingsystem = self.tk.call('tk', 'windowingsystem') | |
63 if windowingsystem == 'aqua': | |
64 try: | |
65 self.tk.call('::tk::unsupported::MacWindowStyle', 'style', | |
66 self._w, 'moveableModal', '') | |
67 except: | |
68 pass | |
69 self.bind("<Command-.>", self.cancel) | |
70 self.bind('<Key-Escape>', self.cancel) | |
71 self.protocol("WM_DELETE_WINDOW", self.cancel) | |
72 self.bind('<Key-Return>', self.ok) | |
73 self.bind("<KP_Enter>", self.ok) | |
74 | |
75 self.create_widgets() | |
76 self.update_idletasks() # Need here for winfo_reqwidth below. | |
77 self.geometry( # Center dialog over parent (or below htest box). | |
78 "+%d+%d" % ( | |
79 parent.winfo_rootx() + | |
80 (parent.winfo_width()/2 - self.winfo_reqwidth()/2), | |
81 parent.winfo_rooty() + | |
82 ((parent.winfo_height()/2 - self.winfo_reqheight()/2) | |
83 if not _htest else 150) | |
84 ) ) | |
85 self.resizable(height=False, width=False) | |
86 | |
87 if not _utest: | |
88 self.deiconify() # Unhide now that geometry set. | |
89 self.wait_window() | |
90 | |
91 def create_widgets(self, ok_text='OK'): # Do not replace. | |
92 """Create entry (rows, extras, buttons. | |
93 | |
94 Entry stuff on rows 0-2, spanning cols 0-2. | |
95 Buttons on row 99, cols 1, 2. | |
96 """ | |
97 # Bind to self the widgets needed for entry_ok or unittest. | |
98 self.frame = frame = Frame(self, padding=10) | |
99 frame.grid(column=0, row=0, sticky='news') | |
100 frame.grid_columnconfigure(0, weight=1) | |
101 | |
102 entrylabel = Label(frame, anchor='w', justify='left', | |
103 text=self.message) | |
104 self.entryvar = StringVar(self, self.text0) | |
105 self.entry = Entry(frame, width=30, textvariable=self.entryvar) | |
106 self.entry.focus_set() | |
107 self.error_font = Font(name='TkCaptionFont', | |
108 exists=True, root=self.parent) | |
109 self.entry_error = Label(frame, text=' ', foreground='red', | |
110 font=self.error_font) | |
111 entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W) | |
112 self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E, | |
113 pady=[10,0]) | |
114 self.entry_error.grid(column=0, row=2, columnspan=3, padx=5, | |
115 sticky=W+E) | |
116 | |
117 self.create_extra() | |
118 | |
119 self.button_ok = Button( | |
120 frame, text=ok_text, default='active', command=self.ok) | |
121 self.button_cancel = Button( | |
122 frame, text='Cancel', command=self.cancel) | |
123 | |
124 self.button_ok.grid(column=1, row=99, padx=5) | |
125 self.button_cancel.grid(column=2, row=99, padx=5) | |
126 | |
127 def create_extra(self): pass # Override to add widgets. | |
128 | |
129 def showerror(self, message, widget=None): | |
130 #self.bell(displayof=self) | |
131 (widget or self.entry_error)['text'] = 'ERROR: ' + message | |
132 | |
133 def entry_ok(self): # Example: usually replace. | |
134 "Return non-blank entry or None." | |
135 self.entry_error['text'] = '' | |
136 entry = self.entry.get().strip() | |
137 if not entry: | |
138 self.showerror('blank line.') | |
139 return None | |
140 return entry | |
141 | |
142 def ok(self, event=None): # Do not replace. | |
143 '''If entry is valid, bind it to 'result' and destroy tk widget. | |
144 | |
145 Otherwise leave dialog open for user to correct entry or cancel. | |
146 ''' | |
147 entry = self.entry_ok() | |
148 if entry is not None: | |
149 self.result = entry | |
150 self.destroy() | |
151 else: | |
152 # [Ok] moves focus. (<Return> does not.) Move it back. | |
153 self.entry.focus_set() | |
154 | |
155 def cancel(self, event=None): # Do not replace. | |
156 "Set dialog result to None and destroy tk widget." | |
157 self.result = None | |
158 self.destroy() | |
159 | |
160 def destroy(self): | |
161 self.grab_release() | |
162 super().destroy() | |
163 | |
164 | |
165 class SectionName(Query): | |
166 "Get a name for a config file section name." | |
167 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837) | |
168 | |
169 def __init__(self, parent, title, message, used_names, | |
170 *, _htest=False, _utest=False): | |
171 super().__init__(parent, title, message, used_names=used_names, | |
172 _htest=_htest, _utest=_utest) | |
173 | |
174 def entry_ok(self): | |
175 "Return sensible ConfigParser section name or None." | |
176 self.entry_error['text'] = '' | |
177 name = self.entry.get().strip() | |
178 if not name: | |
179 self.showerror('no name specified.') | |
180 return None | |
181 elif len(name)>30: | |
182 self.showerror('name is longer than 30 characters.') | |
183 return None | |
184 elif name in self.used_names: | |
185 self.showerror('name is already in use.') | |
186 return None | |
187 return name | |
188 | |
189 | |
190 class ModuleName(Query): | |
191 "Get a module name for Open Module menu entry." | |
192 # Used in open_module (editor.EditorWindow until move to iobinding). | |
193 | |
194 def __init__(self, parent, title, message, text0, | |
195 *, _htest=False, _utest=False): | |
196 super().__init__(parent, title, message, text0=text0, | |
197 _htest=_htest, _utest=_utest) | |
198 | |
199 def entry_ok(self): | |
200 "Return entered module name as file path or None." | |
201 self.entry_error['text'] = '' | |
202 name = self.entry.get().strip() | |
203 if not name: | |
204 self.showerror('no name specified.') | |
205 return None | |
206 # XXX Ought to insert current file's directory in front of path. | |
207 try: | |
208 spec = importlib.util.find_spec(name) | |
209 except (ValueError, ImportError) as msg: | |
210 self.showerror(str(msg)) | |
211 return None | |
212 if spec is None: | |
213 self.showerror("module not found") | |
214 return None | |
215 if not isinstance(spec.loader, importlib.abc.SourceLoader): | |
216 self.showerror("not a source-based module") | |
217 return None | |
218 try: | |
219 file_path = spec.loader.get_filename(name) | |
220 except AttributeError: | |
221 self.showerror("loader does not support get_filename", | |
222 parent=self) | |
223 return None | |
224 return file_path | |
225 | |
226 | |
227 class HelpSource(Query): | |
228 "Get menu name and help source for Help menu." | |
229 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9) | |
230 | |
231 def __init__(self, parent, title, *, menuitem='', filepath='', | |
232 used_names={}, _htest=False, _utest=False): | |
233 """Get menu entry and url/local file for Additional Help. | |
234 | |
235 User enters a name for the Help resource and a web url or file | |
236 name. The user can browse for the file. | |
237 """ | |
238 self.filepath = filepath | |
239 message = 'Name for item on Help menu:' | |
240 super().__init__( | |
241 parent, title, message, text0=menuitem, | |
242 used_names=used_names, _htest=_htest, _utest=_utest) | |
243 | |
244 def create_extra(self): | |
245 "Add path widjets to rows 10-12." | |
246 frame = self.frame | |
247 pathlabel = Label(frame, anchor='w', justify='left', | |
248 text='Help File Path: Enter URL or browse for file') | |
249 self.pathvar = StringVar(self, self.filepath) | |
250 self.path = Entry(frame, textvariable=self.pathvar, width=40) | |
251 browse = Button(frame, text='Browse', width=8, | |
252 command=self.browse_file) | |
253 self.path_error = Label(frame, text=' ', foreground='red', | |
254 font=self.error_font) | |
255 | |
256 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0], | |
257 sticky=W) | |
258 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E, | |
259 pady=[10,0]) | |
260 browse.grid(column=2, row=11, padx=5, sticky=W+S) | |
261 self.path_error.grid(column=0, row=12, columnspan=3, padx=5, | |
262 sticky=W+E) | |
263 | |
264 def askfilename(self, filetypes, initdir, initfile): # htest # | |
265 # Extracted from browse_file so can mock for unittests. | |
266 # Cannot unittest as cannot simulate button clicks. | |
267 # Test by running htest, such as by running this file. | |
268 return filedialog.Open(parent=self, filetypes=filetypes)\ | |
269 .show(initialdir=initdir, initialfile=initfile) | |
270 | |
271 def browse_file(self): | |
272 filetypes = [ | |
273 ("HTML Files", "*.htm *.html", "TEXT"), | |
274 ("PDF Files", "*.pdf", "TEXT"), | |
275 ("Windows Help Files", "*.chm"), | |
276 ("Text Files", "*.txt", "TEXT"), | |
277 ("All Files", "*")] | |
278 path = self.pathvar.get() | |
279 if path: | |
280 dir, base = os.path.split(path) | |
281 else: | |
282 base = None | |
283 if platform[:3] == 'win': | |
284 dir = os.path.join(os.path.dirname(executable), 'Doc') | |
285 if not os.path.isdir(dir): | |
286 dir = os.getcwd() | |
287 else: | |
288 dir = os.getcwd() | |
289 file = self.askfilename(filetypes, dir, base) | |
290 if file: | |
291 self.pathvar.set(file) | |
292 | |
293 item_ok = SectionName.entry_ok # localize for test override | |
294 | |
295 def path_ok(self): | |
296 "Simple validity check for menu file path" | |
297 path = self.path.get().strip() | |
298 if not path: #no path specified | |
299 self.showerror('no help file path specified.', self.path_error) | |
300 return None | |
301 elif not path.startswith(('www.', 'http')): | |
302 if path[:5] == 'file:': | |
303 path = path[5:] | |
304 if not os.path.exists(path): | |
305 self.showerror('help file path does not exist.', | |
306 self.path_error) | |
307 return None | |
308 if platform == 'darwin': # for Mac Safari | |
309 path = "file://" + path | |
310 return path | |
311 | |
312 def entry_ok(self): | |
313 "Return apparently valid (name, path) or None" | |
314 self.entry_error['text'] = '' | |
315 self.path_error['text'] = '' | |
316 name = self.item_ok() | |
317 path = self.path_ok() | |
318 return None if name is None or path is None else (name, path) | |
319 | |
320 class CustomRun(Query): | |
321 """Get settings for custom run of module. | |
322 | |
323 1. Command line arguments to extend sys.argv. | |
324 2. Whether to restart Shell or not. | |
325 """ | |
326 # Used in runscript.run_custom_event | |
327 | |
328 def __init__(self, parent, title, *, cli_args=[], | |
329 _htest=False, _utest=False): | |
330 """cli_args is a list of strings. | |
331 | |
332 The list is assigned to the default Entry StringVar. | |
333 The strings are displayed joined by ' ' for display. | |
334 """ | |
335 message = 'Command Line Arguments for sys.argv:' | |
336 super().__init__( | |
337 parent, title, message, text0=cli_args, | |
338 _htest=_htest, _utest=_utest) | |
339 | |
340 def create_extra(self): | |
341 "Add run mode on rows 10-12." | |
342 frame = self.frame | |
343 self.restartvar = BooleanVar(self, value=True) | |
344 restart = Checkbutton(frame, variable=self.restartvar, onvalue=True, | |
345 offvalue=False, text='Restart shell') | |
346 self.args_error = Label(frame, text=' ', foreground='red', | |
347 font=self.error_font) | |
348 | |
349 restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w') | |
350 self.args_error.grid(column=0, row=12, columnspan=3, padx=5, | |
351 sticky='we') | |
352 | |
353 def cli_args_ok(self): | |
354 "Validity check and parsing for command line arguments." | |
355 cli_string = self.entry.get().strip() | |
356 try: | |
357 cli_args = shlex.split(cli_string, posix=True) | |
358 except ValueError as err: | |
359 self.showerror(str(err)) | |
360 return None | |
361 return cli_args | |
362 | |
363 def entry_ok(self): | |
364 "Return apparently valid (cli_args, restart) or None" | |
365 self.entry_error['text'] = '' | |
366 cli_args = self.cli_args_ok() | |
367 restart = self.restartvar.get() | |
368 return None if cli_args is None else (cli_args, restart) | |
369 | |
370 | |
371 if __name__ == '__main__': | |
372 from unittest import main | |
373 main('idlelib.idle_test.test_query', verbosity=2, exit=False) | |
374 | |
375 from idlelib.idle_test.htest import run | |
376 run(Query, HelpSource, CustomRun) |