diff options
Diffstat (limited to 'Lib/idlelib/query.py')
-rw-r--r-- | Lib/idlelib/query.py | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/Lib/idlelib/query.py b/Lib/idlelib/query.py new file mode 100644 index 0000000..3b1f1e2 --- /dev/null +++ b/Lib/idlelib/query.py @@ -0,0 +1,308 @@ +""" +Dialogs that query users and verify the answer before accepting. +Use ttk widgets, limiting use to tcl/tk 8.5+, as in IDLE 3.6+. + +Query is the generic base class for a popup dialog. +The user must either enter a valid answer or close the dialog. +Entries are validated when <Return> is entered or [Ok] is clicked. +Entries are ignored when [Cancel] or [X] are clicked. +The 'return value' is .result set to either a valid answer or None. + +Subclass SectionName gets a name for a new config file section. +Configdialog uses it for new highlight theme and keybinding set names. +Subclass ModuleName gets a name for File => Open Module. +Subclass HelpSource gets menu item and path for additions to Help menu. +""" +# Query and Section name result from splitting GetCfgSectionNameDialog +# of configSectionNameDialog.py (temporarily config_sec.py) into +# generic and specific parts. 3.6 only, July 2016. +# ModuleName.entry_ok came from editor.EditorWindow.load_module. +# HelpSource was extracted from configHelpSourceEdit.py (temporarily +# config_help.py), with darwin code moved from ok to path_ok. + +import importlib +import os +from sys import executable, platform # Platform is set for one test. + +from tkinter import Toplevel, StringVar, W, E, N, S +from tkinter.ttk import Frame, Button, Entry, Label +from tkinter import filedialog +from tkinter.font import Font + +class Query(Toplevel): + """Base class for getting verified answer from a user. + + For this base class, accept any non-blank string. + """ + def __init__(self, parent, title, message, *, text0='', used_names={}, + _htest=False, _utest=False): + """Create popup, do not return until tk widget destroyed. + + Additional subclass init must be done before calling this + unless _utest=True is passed to suppress wait_window(). + + title - string, title of popup dialog + message - string, informational message to display + text0 - initial value for entry + used_names - names already in use + _htest - bool, change box location when running htest + _utest - bool, leave window hidden and not modal + """ + Toplevel.__init__(self, parent) + self.withdraw() # Hide while configuring, especially geometry. + self.parent = parent + self.title(title) + self.message = message + self.text0 = text0 + self.used_names = used_names + self.transient(parent) + self.grab_set() + windowingsystem = self.tk.call('tk', 'windowingsystem') + if windowingsystem == 'aqua': + try: + self.tk.call('::tk::unsupported::MacWindowStyle', 'style', + self._w, 'moveableModal', '') + except: + pass + self.bind("<Command-.>", self.cancel) + self.bind('<Key-Escape>', self.cancel) + self.protocol("WM_DELETE_WINDOW", self.cancel) + self.bind('<Key-Return>', self.ok) + self.bind("<KP_Enter>", self.ok) + self.resizable(height=False, width=False) + self.create_widgets() + self.update_idletasks() # Needed here for winfo_reqwidth below. + self.geometry( # Center dialog over parent (or below htest box). + "+%d+%d" % ( + parent.winfo_rootx() + + (parent.winfo_width()/2 - self.winfo_reqwidth()/2), + parent.winfo_rooty() + + ((parent.winfo_height()/2 - self.winfo_reqheight()/2) + if not _htest else 150) + ) ) + if not _utest: + self.deiconify() # Unhide now that geometry set. + self.wait_window() + + def create_widgets(self): # Call from override, if any. + # Bind to self widgets needed for entry_ok or unittest. + self.frame = frame = Frame(self, padding=10) + frame.grid(column=0, row=0, sticky='news') + frame.grid_columnconfigure(0, weight=1) + + entrylabel = Label(frame, anchor='w', justify='left', + text=self.message) + self.entryvar = StringVar(self, self.text0) + self.entry = Entry(frame, width=30, textvariable=self.entryvar) + self.entry.focus_set() + self.error_font = Font(name='TkCaptionFont', + exists=True, root=self.parent) + self.entry_error = Label(frame, text=' ', foreground='red', + font=self.error_font) + self.button_ok = Button( + frame, text='OK', default='active', command=self.ok) + self.button_cancel = Button( + frame, text='Cancel', command=self.cancel) + + entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W) + self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E, + pady=[10,0]) + self.entry_error.grid(column=0, row=2, columnspan=3, padx=5, + sticky=W+E) + self.button_ok.grid(column=1, row=99, padx=5) + self.button_cancel.grid(column=2, row=99, padx=5) + + def showerror(self, message, widget=None): + #self.bell(displayof=self) + (widget or self.entry_error)['text'] = 'ERROR: ' + message + + def entry_ok(self): # Example: usually replace. + "Return non-blank entry or None." + self.entry_error['text'] = '' + entry = self.entry.get().strip() + if not entry: + self.showerror('blank line.') + return None + return entry + + def ok(self, event=None): # Do not replace. + '''If entry is valid, bind it to 'result' and destroy tk widget. + + Otherwise leave dialog open for user to correct entry or cancel. + ''' + entry = self.entry_ok() + if entry is not None: + self.result = entry + self.destroy() + else: + # [Ok] moves focus. (<Return> does not.) Move it back. + self.entry.focus_set() + + def cancel(self, event=None): # Do not replace. + "Set dialog result to None and destroy tk widget." + self.result = None + self.destroy() + + +class SectionName(Query): + "Get a name for a config file section name." + # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837) + + def __init__(self, parent, title, message, used_names, + *, _htest=False, _utest=False): + super().__init__(parent, title, message, used_names=used_names, + _htest=_htest, _utest=_utest) + + def entry_ok(self): + "Return sensible ConfigParser section name or None." + self.entry_error['text'] = '' + name = self.entry.get().strip() + if not name: + self.showerror('no name specified.') + return None + elif len(name)>30: + self.showerror('name is longer than 30 characters.') + return None + elif name in self.used_names: + self.showerror('name is already in use.') + return None + return name + + +class ModuleName(Query): + "Get a module name for Open Module menu entry." + # Used in open_module (editor.EditorWindow until move to iobinding). + + def __init__(self, parent, title, message, text0, + *, _htest=False, _utest=False): + super().__init__(parent, title, message, text0=text0, + _htest=_htest, _utest=_utest) + + def entry_ok(self): + "Return entered module name as file path or None." + self.entry_error['text'] = '' + name = self.entry.get().strip() + if not name: + self.showerror('no name specified.') + return None + # XXX Ought to insert current file's directory in front of path. + try: + spec = importlib.util.find_spec(name) + except (ValueError, ImportError) as msg: + self.showerror(str(msg)) + return None + if spec is None: + self.showerror("module not found") + return None + if not isinstance(spec.loader, importlib.abc.SourceLoader): + self.showerror("not a source-based module") + return None + try: + file_path = spec.loader.get_filename(name) + except AttributeError: + self.showerror("loader does not support get_filename", + parent=self) + return None + return file_path + + +class HelpSource(Query): + "Get menu name and help source for Help menu." + # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9) + + def __init__(self, parent, title, *, menuitem='', filepath='', + used_names={}, _htest=False, _utest=False): + """Get menu entry and url/local file for Additional Help. + + User enters a name for the Help resource and a web url or file + name. The user can browse for the file. + """ + self.filepath = filepath + message = 'Name for item on Help menu:' + super().__init__( + parent, title, message, text0=menuitem, + used_names=used_names, _htest=_htest, _utest=_utest) + + def create_widgets(self): + super().create_widgets() + frame = self.frame + pathlabel = Label(frame, anchor='w', justify='left', + text='Help File Path: Enter URL or browse for file') + self.pathvar = StringVar(self, self.filepath) + self.path = Entry(frame, textvariable=self.pathvar, width=40) + browse = Button(frame, text='Browse', width=8, + command=self.browse_file) + self.path_error = Label(frame, text=' ', foreground='red', + font=self.error_font) + + pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0], + sticky=W) + self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E, + pady=[10,0]) + browse.grid(column=2, row=11, padx=5, sticky=W+S) + self.path_error.grid(column=0, row=12, columnspan=3, padx=5, + sticky=W+E) + + def askfilename(self, filetypes, initdir, initfile): # htest # + # Extracted from browse_file so can mock for unittests. + # Cannot unittest as cannot simulate button clicks. + # Test by running htest, such as by running this file. + return filedialog.Open(parent=self, filetypes=filetypes)\ + .show(initialdir=initdir, initialfile=initfile) + + def browse_file(self): + filetypes = [ + ("HTML Files", "*.htm *.html", "TEXT"), + ("PDF Files", "*.pdf", "TEXT"), + ("Windows Help Files", "*.chm"), + ("Text Files", "*.txt", "TEXT"), + ("All Files", "*")] + path = self.pathvar.get() + if path: + dir, base = os.path.split(path) + else: + base = None + if platform[:3] == 'win': + dir = os.path.join(os.path.dirname(executable), 'Doc') + if not os.path.isdir(dir): + dir = os.getcwd() + else: + dir = os.getcwd() + file = self.askfilename(filetypes, dir, base) + if file: + self.pathvar.set(file) + + item_ok = SectionName.entry_ok # localize for test override + + def path_ok(self): + "Simple validity check for menu file path" + path = self.path.get().strip() + if not path: #no path specified + self.showerror('no help file path specified.', self.path_error) + return None + elif not path.startswith(('www.', 'http')): + if path[:5] == 'file:': + path = path[5:] + if not os.path.exists(path): + self.showerror('help file path does not exist.', + self.path_error) + return None + if platform == 'darwin': # for Mac Safari + path = "file://" + path + return path + + def entry_ok(self): + "Return apparently valid (name, path) or None" + self.entry_error['text'] = '' + self.path_error['text'] = '' + name = self.item_ok() + path = self.path_ok() + return None if name is None or path is None else (name, path) + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(Query, HelpSource) |