codekingpro/portable-devtools
114k
1"""2Dialogs that query users and verify the answer before accepting.3 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.12Subclass ModuleName gets a name for File => Open Module.13Subclass HelpSource gets menu item and path for additions to Help menu.14"""15# Query and Section name result from splitting GetCfgSectionNameDialog16# of configSectionNameDialog.py (temporarily config_sec.py) into17# 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 (temporarily20# config_help.py), with darwin code moved from ok to path_ok.21 22import importlib.util, importlib.abc23import os24import shlex25from sys import executable, platform # Platform is set for one test.26 27from tkinter import Toplevel, StringVar, BooleanVar, W, E, S28from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton29from tkinter import filedialog30from tkinter.font import Font31from tkinter.simpledialog import _setup_dialog32 33class Query(Toplevel):34 """Base class for getting verified answer from a user.35 36 For this base class, accept any non-blank string.37 """38 def __init__(self, parent, title, message, *, text0='', used_names={},39 _htest=False, _utest=False):40 """Create modal popup, return when destroyed.41 42 Additional subclass init must be done before this unless43 _utest=True is passed to suppress wait_window().44 45 title - string, title of popup dialog46 message - string, informational message to display47 text0 - initial value for entry48 used_names - names already in use49 _htest - bool, change box location when running htest50 _utest - bool, leave window hidden and not modal51 """52 self.parent = parent # Needed for Font call.53 self.message = message54 self.text0 = text055 self.used_names = used_names56 57 Toplevel.__init__(self, parent)58 self.withdraw() # Hide while configuring, especially geometry.59 self.title(title)60 self.transient(parent)61 if not _utest: # Otherwise fail when directly run unittest.62 self.grab_set()63 64 _setup_dialog(self)65 if self._windowingsystem == 'aqua':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 72 self.create_widgets()73 self.update_idletasks() # Need here for winfo_reqwidth below.74 self.geometry( # Center dialog over parent (or below htest box).75 "+%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)81 ) )82 self.resizable(height=False, width=False)83 84 if not _utest:85 self.deiconify() # Unhide now that geometry set.86 self.entry.focus_set()87 self.wait_window()88 89 def create_widgets(self, ok_text='OK'): # Do not replace.90 """Create entry (rows, extras, buttons.91 92 Entry stuff on rows 0-2, spanning cols 0-2.93 Buttons on row 99, cols 1, 2.94 """95 # Bind to self the widgets needed for entry_ok or unittest.96 self.frame = frame = Frame(self, padding=10)97 frame.grid(column=0, row=0, sticky='news')98 frame.grid_columnconfigure(0, weight=1)99 100 entrylabel = Label(frame, anchor='w', justify='left',101 text=self.message)102 self.entryvar = StringVar(self, self.text0)103 self.entry = Entry(frame, width=30, textvariable=self.entryvar)104 self.error_font = Font(name='TkCaptionFont',105 exists=True, root=self.parent)106 self.entry_error = Label(frame, text=' ', foreground='red',107 font=self.error_font)108 # Display or blank error by setting ['text'] =.109 entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)110 self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,111 pady=[10,0])112 self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,113 sticky=W+E)114 115 self.create_extra()116 117 self.button_ok = Button(118 frame, text=ok_text, default='active', command=self.ok)119 self.button_cancel = Button(120 frame, text='Cancel', command=self.cancel)121 122 self.button_ok.grid(column=1, row=99, padx=5)123 self.button_cancel.grid(column=2, row=99, padx=5)124 125 def create_extra(self): pass # Override to add widgets.126 127 def showerror(self, message, widget=None):128 #self.bell(displayof=self)129 (widget or self.entry_error)['text'] = 'ERROR: ' + message130 131 def entry_ok(self): # Example: usually replace.132 "Return non-blank entry or None."133 entry = self.entry.get().strip()134 if not entry:135 self.showerror('blank line.')136 return None137 return entry138 139 def ok(self, event=None): # Do not replace.140 '''If entry is valid, bind it to 'result' and destroy tk widget.141 142 Otherwise leave dialog open for user to correct entry or cancel.143 '''144 self.entry_error['text'] = ''145 entry = self.entry_ok()146 if entry is not None:147 self.result = entry148 self.destroy()149 else:150 # [Ok] moves focus. (<Return> does not.) Move it back.151 self.entry.focus_set()152 153 def cancel(self, event=None): # Do not replace.154 "Set dialog result to None and destroy tk widget."155 self.result = None156 self.destroy()157 158 def destroy(self):159 self.grab_release()160 super().destroy()161 162 163class SectionName(Query):164 "Get a name for a config file section name."165 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)166 167 def __init__(self, parent, title, message, used_names,168 *, _htest=False, _utest=False):169 super().__init__(parent, title, message, used_names=used_names,170 _htest=_htest, _utest=_utest)171 172 def entry_ok(self):173 "Return sensible ConfigParser section name or None."174 name = self.entry.get().strip()175 if not name:176 self.showerror('no name specified.')177 return None178 elif len(name)>30:179 self.showerror('name is longer than 30 characters.')180 return None181 elif name in self.used_names:182 self.showerror('name is already in use.')183 return None184 return name185 186 187class ModuleName(Query):188 "Get a module name for Open Module menu entry."189 # Used in open_module (editor.EditorWindow until move to iobinding).190 191 def __init__(self, parent, title, message, text0,192 *, _htest=False, _utest=False):193 super().__init__(parent, title, message, text0=text0,194 _htest=_htest, _utest=_utest)195 196 def entry_ok(self):197 "Return entered module name as file path or None."198 name = self.entry.get().strip()199 if not name:200 self.showerror('no name specified.')201 return None202 # XXX Ought to insert current file's directory in front of path.203 try:204 spec = importlib.util.find_spec(name)205 except (ValueError, ImportError) as msg:206 self.showerror(str(msg))207 return None208 if spec is None:209 self.showerror("module not found.")210 return None211 if not isinstance(spec.loader, importlib.abc.SourceLoader):212 self.showerror("not a source-based module.")213 return None214 try:215 file_path = spec.loader.get_filename(name)216 except AttributeError:217 self.showerror("loader does not support get_filename.")218 return None219 except ImportError:220 # Some special modules require this (e.g. os.path)221 try:222 file_path = spec.loader.get_filename()223 except TypeError:224 self.showerror("loader failed to get filename.")225 return None226 return file_path227 228 229class Goto(Query):230 "Get a positive line number for editor Go To Line."231 # Used in editor.EditorWindow.goto_line_event.232 233 def entry_ok(self):234 try:235 lineno = int(self.entry.get())236 except ValueError:237 self.showerror('not a base 10 integer.')238 return None239 if lineno <= 0:240 self.showerror('not a positive integer.')241 return None242 return lineno243 244 245class HelpSource(Query):246 "Get menu name and help source for Help menu."247 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)248 249 def __init__(self, parent, title, *, menuitem='', filepath='',250 used_names={}, _htest=False, _utest=False):251 """Get menu entry and url/local file for Additional Help.252 253 User enters a name for the Help resource and a web url or file254 name. The user can browse for the file.255 """256 self.filepath = filepath257 message = 'Name for item on Help menu:'258 super().__init__(259 parent, title, message, text0=menuitem,260 used_names=used_names, _htest=_htest, _utest=_utest)261 262 def create_extra(self):263 "Add path widjets to rows 10-12."264 frame = self.frame265 pathlabel = Label(frame, anchor='w', justify='left',266 text='Help File Path: Enter URL or browse for file')267 self.pathvar = StringVar(self, self.filepath)268 self.path = Entry(frame, textvariable=self.pathvar, width=40)269 browse = Button(frame, text='Browse', width=8,270 command=self.browse_file)271 self.path_error = Label(frame, text=' ', foreground='red',272 font=self.error_font)273 274 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],275 sticky=W)276 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,277 pady=[10,0])278 browse.grid(column=2, row=11, padx=5, sticky=W+S)279 self.path_error.grid(column=0, row=12, columnspan=3, padx=5,280 sticky=W+E)281 282 def askfilename(self, filetypes, initdir, initfile): # htest #283 # Extracted from browse_file so can mock for unittests.284 # Cannot unittest as cannot simulate button clicks.285 # Test by running htest, such as by running this file.286 return filedialog.Open(parent=self, filetypes=filetypes)\287 .show(initialdir=initdir, initialfile=initfile)288 289 def browse_file(self):290 filetypes = [291 ("HTML Files", "*.htm *.html", "TEXT"),292 ("Text Files", "*.txt", "TEXT"),293 ("All Files", "*")]294 path = self.pathvar.get()295 if path:296 dir, base = os.path.split(path)297 else:298 base = None299 if platform[:3] == 'win':300 dir = os.path.join(os.path.dirname(executable), 'Doc')301 if not os.path.isdir(dir):302 dir = os.getcwd()303 else:304 dir = os.getcwd()305 file = self.askfilename(filetypes, dir, base)306 if file:307 self.pathvar.set(file)308 309 item_ok = SectionName.entry_ok # localize for test override310 311 def path_ok(self):312 "Simple validity check for menu file path"313 path = self.path.get().strip()314 if not path: #no path specified315 self.showerror('no help file path specified.', self.path_error)316 return None317 elif not path.startswith(('www.', 'http')):318 if path[:5] == 'file:':319 path = path[5:]320 if not os.path.exists(path):321 self.showerror('help file path does not exist.',322 self.path_error)323 return None324 if platform == 'darwin': # for Mac Safari325 path = "file://" + path326 return path327 328 def entry_ok(self):329 "Return apparently valid (name, path) or None"330 self.path_error['text'] = ''331 name = self.item_ok()332 path = self.path_ok()333 return None if name is None or path is None else (name, path)334 335class CustomRun(Query):336 """Get settings for custom run of module.337 338 1. Command line arguments to extend sys.argv.339 2. Whether to restart Shell or not.340 """341 # Used in runscript.run_custom_event342 343 def __init__(self, parent, title, *, cli_args=[],344 _htest=False, _utest=False):345 """cli_args is a list of strings.346 347 The list is assigned to the default Entry StringVar.348 The strings are displayed joined by ' ' for display.349 """350 message = 'Command Line Arguments for sys.argv:'351 super().__init__(352 parent, title, message, text0=cli_args,353 _htest=_htest, _utest=_utest)354 355 def create_extra(self):356 "Add run mode on rows 10-12."357 frame = self.frame358 self.restartvar = BooleanVar(self, value=True)359 restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,360 offvalue=False, text='Restart shell')361 self.args_error = Label(frame, text=' ', foreground='red',362 font=self.error_font)363 364 restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')365 self.args_error.grid(column=0, row=12, columnspan=3, padx=5,366 sticky='we')367 368 def cli_args_ok(self):369 "Return command line arg list or None if error."370 cli_string = self.entry.get().strip()371 try:372 cli_args = shlex.split(cli_string, posix=True)373 except ValueError as err:374 self.showerror(str(err))375 return None376 return cli_args377 378 def entry_ok(self):379 "Return apparently valid (cli_args, restart) or None."380 cli_args = self.cli_args_ok()381 restart = self.restartvar.get()382 return None if cli_args is None else (cli_args, restart)383 384 385if __name__ == '__main__':386 from unittest import main387 main('idlelib.idle_test.test_query', verbosity=2, exit=False)388 389 from idlelib.idle_test.htest import run390 run(Query, HelpSource, CustomRun)391 