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