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)