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