1""" 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. 13Subclass ModuleName gets a name for File => Open Module. 14Subclass HelpSource gets menu item and path for additions to Help menu. 15""" 16# Query and Section name result from splitting GetCfgSectionNameDialog 17# of configSectionNameDialog.py (temporarily config_sec.py) into 18# 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. 22 23import importlib 24import os 25from sys import executable, platform # Platform is set for one test. 26 27from tkinter import Toplevel, StringVar, W, E, N, S 28from tkinter.ttk import Frame, Button, Entry, Label 29from tkinter import filedialog 30from tkinter.font import Font 31 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 """ 37 def __init__(self, parent, title, message, *, text0='', used_names={}, 38 _htest=False, _utest=False): 39 """Create popup, do not return until tk widget destroyed. 40 41 Additional subclass init must be done before calling this 42 unless _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 Toplevel.__init__(self, parent) 52 self.withdraw() # Hide while configuring, especially geometry. 53 self.parent = parent 54 self.title(title) 55 self.message = message 56 self.text0 = text0 57 self.used_names = used_names 58 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) 73 self.create_widgets() 74 self.update_idletasks() # Needed here for winfo_reqwidth below. 75 self.geometry( # Center dialog over parent (or below htest box). 76 "+%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) 82 ) ) 83 if not _utest: 84 self.deiconify() # Unhide now that geometry set. 85 self.wait_window() 86 87 def create_widgets(self): # Call from override, if any. 88 # Bind to self widgets needed for entry_ok or unittest. 89 self.frame = frame = Frame(self, padding=10) 90 frame.grid(column=0, row=0, sticky='news') 91 frame.grid_columnconfigure(0, weight=1) 92 93 entrylabel = Label(frame, anchor='w', justify='left', 94 text=self.message) 95 self.entryvar = StringVar(self, self.text0) 96 self.entry = Entry(frame, width=30, textvariable=self.entryvar) 97 self.entry.focus_set() 98 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) 106 107 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) 114 115 def showerror(self, message, widget=None): 116 #self.bell(displayof=self) 117 (widget or self.entry_error)['text'] = 'ERROR: ' + message 118 119 def entry_ok(self): # Example: usually replace. 120 "Return non-blank entry or None." 121 self.entry_error['text'] = '' 122 entry = self.entry.get().strip() 123 if not entry: 124 self.showerror('blank line.') 125 return None 126 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() 134 if entry is not None: 135 self.result = entry 136 self.destroy() 137 else: 138 # [Ok] moves focus. (<Return> does not.) Move it back. 139 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 146 147class SectionName(Query): 148 "Get a name for a config file section name." 149 # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837) 150 151 def __init__(self, parent, title, message, used_names, 152 *, _htest=False, _utest=False): 153 super().__init__(parent, title, message, used_names=used_names, 154 _htest=_htest, _utest=_utest) 155 156 def entry_ok(self): 157 "Return sensible ConfigParser section name or None." 158 self.entry_error['text'] = '' 159 name = self.entry.get().strip() 160 if not name: 161 self.showerror('no name specified.') 162 return None 163 elif len(name)>30: 164 self.showerror('name is longer than 30 characters.') 165 return None 166 elif name in self.used_names: 167 self.showerror('name is already in use.') 168 return None 169 return name 170 171 172class ModuleName(Query): 173 "Get a module name for Open Module menu entry." 174 # Used in open_module (editor.EditorWindow until move to iobinding). 175 176 def __init__(self, parent, title, message, text0, 177 *, _htest=False, _utest=False): 178 super().__init__(parent, title, message, text0=text0, 179 _htest=_htest, _utest=_utest) 180 181 def entry_ok(self): 182 "Return entered module name as file path or None." 183 self.entry_error['text'] = '' 184 name = self.entry.get().strip() 185 if not name: 186 self.showerror('no name specified.') 187 return None 188 # XXX Ought to insert current file's directory in front of path. 189 try: 190 spec = importlib.util.find_spec(name) 191 except (ValueError, ImportError) as msg: 192 self.showerror(str(msg)) 193 return None 194 if spec is None: 195 self.showerror("module not found") 196 return None 197 if not isinstance(spec.loader, importlib.abc.SourceLoader): 198 self.showerror("not a source-based module") 199 return None 200 try: 201 file_path = spec.loader.get_filename(name) 202 except AttributeError: 203 self.showerror("loader does not support get_filename", 204 parent=self) 205 return None 206 return file_path 207 208 209class HelpSource(Query): 210 "Get menu name and help source for Help menu." 211 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9) 212 213 def __init__(self, parent, title, *, menuitem='', filepath='', 214 used_names={}, _htest=False, _utest=False): 215 """Get menu entry and url/local file for Additional Help. 216 217 User enters a name for the Help resource and a web url or file 218 name. The user can browse for the file. 219 """ 220 self.filepath = filepath 221 message = 'Name for item on Help menu:' 222 super().__init__( 223 parent, title, message, text0=menuitem, 224 used_names=used_names, _htest=_htest, _utest=_utest) 225 226 def create_widgets(self): 227 super().create_widgets() 228 frame = self.frame 229 pathlabel = Label(frame, anchor='w', justify='left', 230 text='Help File Path: Enter URL or browse for file') 231 self.pathvar = StringVar(self, self.filepath) 232 self.path = Entry(frame, textvariable=self.pathvar, width=40) 233 browse = Button(frame, text='Browse', width=8, 234 command=self.browse_file) 235 self.path_error = Label(frame, text=' ', foreground='red', 236 font=self.error_font) 237 238 pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0], 239 sticky=W) 240 self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E, 241 pady=[10,0]) 242 browse.grid(column=2, row=11, padx=5, sticky=W+S) 243 self.path_error.grid(column=0, row=12, columnspan=3, padx=5, 244 sticky=W+E) 245 246 def askfilename(self, filetypes, initdir, initfile): # htest # 247 # Extracted from browse_file so can mock for unittests. 248 # Cannot unittest as cannot simulate button clicks. 249 # Test by running htest, such as by running this file. 250 return filedialog.Open(parent=self, filetypes=filetypes)\ 251 .show(initialdir=initdir, initialfile=initfile) 252 253 def browse_file(self): 254 filetypes = [ 255 ("HTML Files", "*.htm *.html", "TEXT"), 256 ("PDF Files", "*.pdf", "TEXT"), 257 ("Windows Help Files", "*.chm"), 258 ("Text Files", "*.txt", "TEXT"), 259 ("All Files", "*")] 260 path = self.pathvar.get() 261 if path: 262 dir, base = os.path.split(path) 263 else: 264 base = None 265 if platform[:3] == 'win': 266 dir = os.path.join(os.path.dirname(executable), 'Doc') 267 if not os.path.isdir(dir): 268 dir = os.getcwd() 269 else: 270 dir = os.getcwd() 271 file = self.askfilename(filetypes, dir, base) 272 if file: 273 self.pathvar.set(file) 274 275 item_ok = SectionName.entry_ok # localize for test override 276 277 def path_ok(self): 278 "Simple validity check for menu file path" 279 path = self.path.get().strip() 280 if not path: #no path specified 281 self.showerror('no help file path specified.', self.path_error) 282 return None 283 elif not path.startswith(('www.', 'http')): 284 if path[:5] == 'file:': 285 path = path[5:] 286 if not os.path.exists(path): 287 self.showerror('help file path does not exist.', 288 self.path_error) 289 return None 290 if platform == 'darwin': # for Mac Safari 291 path = "file://" + path 292 return path 293 294 def entry_ok(self): 295 "Return apparently valid (name, path) or None" 296 self.entry_error['text'] = '' 297 self.path_error['text'] = '' 298 name = self.item_ok() 299 path = self.path_ok() 300 return None if name is None or path is None else (name, path) 301 302 303if __name__ == '__main__': 304 import unittest 305 unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False) 306 307 from idlelib.idle_test.htest import run 308 run(Query, HelpSource) 309