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