dialogs that query users and verify the answer before accepting 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 configsec py into generic and specific parts 3 6 only july 2016 modulename entryok came from editor editorwindow loadmodule helpsource was extracted from confighelpsourceedit py temporarily confighelp py with darwin code moved from ok to pathok base class for getting verified answer from a user for this base class accept any nonblank string create modal popup return when destroyed additional subclass init must be done before this unless utesttrue is passed to suppress waitwindow title string title of popup dialog message string informational message to display text0 initial value for entry usednames names already in use htest bool change box location when running htest utest bool leave window hidden and not modal create entry rows extras buttons entry stuff on rows 02 spanning cols 02 buttons on row 99 cols 1 2 bind to self the widgets needed for entryok or unittest display or blank error by setting text self belldisplayofself if entry is valid bind it to result and destroy tk widget otherwise leave dialog open for user to correct entry or cancel ok moves focus return does not move it back used in configdialog getnewkeysname getnewthemename 837 used in openmodule editor editorwindow until move to iobinding xxx ought to insert current file s directory in front of path some special modules require this e g os path used in editor editorwindow gotolineevent used in configdialog helplistitemaddedit 9419 get menu entry and urllocal 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 extracted from browsefile so can mock for unittests cannot unittest as cannot simulate button clicks test by running htest such as by running this file get settings for custom run of module 1 command line arguments to extend sys argv 2 whether to restart shell or not used in runscript runcustomevent cliargs is a list of strings the list is assigned to the default entry stringvar the strings are displayed joined by for display 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 platform is set for one test base class for getting verified answer from a user for this base class accept any non blank string create modal popup return when destroyed additional subclass init must be done before 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 needed for font call hide while configuring especially geometry otherwise fail when directly run unittest need here for winfo_reqwidth below center dialog over parent or below htest box unhide now that geometry set do not replace create entry rows extras buttons entry stuff on rows 0 2 spanning cols 0 2 buttons on row 99 cols 1 2 bind to self the widgets needed for entry_ok or unittest display or blank error by setting text override to add widgets self bell displayof self example usually replace 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 ok moves focus return does not move it back do not replace used in configdialog getnewkeysname getnewthemename 837 used in open_module editor editorwindow until move to iobinding xxx ought to insert current file s directory in front of path some special modules require this e g os path used in editor editorwindow goto_line_event used in configdialog helplistitemadd edit 941 9 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 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 localize for test override no path specified for mac safari get settings for custom run of module 1 command line arguments to extend sys argv 2 whether to restart shell or not used in runscript run_custom_event cli_args is a list of strings the list is assigned to the default entry stringvar the strings are displayed joined by for display
import importlib.util, importlib.abc
import os
import shlex
from sys import executable, platform  

from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
from tkinter import filedialog
from tkinter.font import Font
from tkinter.simpledialog import _setup_dialog

class Query(Toplevel):
    
    def __init__(self, parent, title, message, *, text0='', used_names={},
                 _htest=False, _utest=False):
        
        self.parent = parent  
        self.message = message
        self.text0 = text0
        self.used_names = used_names

        Toplevel.__init__(self, parent)
        self.withdraw()  
        self.title(title)
        self.transient(parent)
        if not _utest:  
            self.grab_set()

        _setup_dialog(self)
        if self._windowingsystem == 'aqua':
            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.create_widgets()
        self.update_idletasks()  
        self.geometry(  
                "+%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)
                ) )
        self.resizable(height=False, width=False)

        if not _utest:
            self.deiconify()  
            self.entry.focus_set()
            self.wait_window()

    def create_widgets(self, ok_text='OK'):  
        
        
        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.error_font = Font(name='TkCaptionFont',
                               exists=True, root=self.parent)
        self.entry_error = Label(frame, text=' ', foreground='red',
                                 font=self.error_font)
        
        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.create_extra()

        self.button_ok = Button(
                frame, text=ok_text, default='active', command=self.ok)
        self.button_cancel = Button(
                frame, text='Cancel', command=self.cancel)

        self.button_ok.grid(column=1, row=99, padx=5)
        self.button_cancel.grid(column=2, row=99, padx=5)

    def create_extra(self): pass  

    def showerror(self, message, widget=None):
        
        (widget or self.entry_error)['text'] = 'ERROR: ' + message

    def entry_ok(self):  
        "Return non-blank entry or None."
        entry = self.entry.get().strip()
        if not entry:
            self.showerror('blank line.')
            return None
        return entry

    def ok(self, event=None):  
        
        self.entry_error['text'] = ''
        entry = self.entry_ok()
        if entry is not None:
            self.result = entry
            self.destroy()
        else:
            
            self.entry.focus_set()

    def cancel(self, event=None):  
        "Set dialog result to None and destroy tk widget."
        self.result = None
        self.destroy()

    def destroy(self):
        self.grab_release()
        super().destroy()


class SectionName(Query):
    "Get a name for a config file section name."
    

    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."
        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."
    

    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."
        name = self.entry.get().strip()
        if not name:
            self.showerror('no name specified.')
            return None
        
        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.")
            return None
        except ImportError:
            
            try:
                file_path = spec.loader.get_filename()
            except TypeError:
                self.showerror("loader failed to get filename.")
                return None
        return file_path


class Goto(Query):
    "Get a positive line number for editor Go To Line."
    

    def entry_ok(self):
        try:
            lineno = int(self.entry.get())
        except ValueError:
            self.showerror('not a base 10 integer.')
            return None
        if lineno <= 0:
            self.showerror('not a positive integer.')
            return None
        return lineno


class HelpSource(Query):
    "Get menu name and help source for Help menu."
    

    def __init__(self, parent, title, *, menuitem='', filepath='',
                 used_names={}, _htest=False, _utest=False):
        
        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_extra(self):
        "Add path widjets to rows 10-12."
        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):  
        
        
        
        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  

    def path_ok(self):
        "Simple validity check for menu file path"
        path = self.path.get().strip()
        if not path: 
            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':  
                path =  "file://" + path
        return path

    def entry_ok(self):
        "Return apparently valid (name, path) or None"
        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)

class CustomRun(Query):
    
    

    def __init__(self, parent, title, *, cli_args=[],
                 _htest=False, _utest=False):
        
        message = 'Command Line Arguments for sys.argv:'
        super().__init__(
                parent, title, message, text0=cli_args,
                _htest=_htest, _utest=_utest)

    def create_extra(self):
        "Add run mode on rows 10-12."
        frame = self.frame
        self.restartvar = BooleanVar(self, value=True)
        restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
                              offvalue=False, text='Restart shell')
        self.args_error = Label(frame, text=' ', foreground='red',
                                font=self.error_font)

        restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
        self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
                             sticky='we')

    def cli_args_ok(self):
        "Return command line arg list or None if error."
        cli_string = self.entry.get().strip()
        try:
            cli_args = shlex.split(cli_string, posix=True)
        except ValueError as err:
            self.showerror(str(err))
            return None
        return cli_args

    def entry_ok(self):
        "Return apparently valid (cli_args, restart) or None."
        cli_args = self.cli_args_ok()
        restart = self.restartvar.get()
        return None if cli_args is None else (cli_args, restart)


if __name__ == '__main__':
    from unittest import main
    main('idlelib.idle_test.test_query', verbosity=2, exit=False)

    from idlelib.idle_test.htest import run
    run(Query, HelpSource, CustomRun)