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