123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- """
- 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 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.util, importlib.abc
- import os
- import shlex
- from sys import executable, platform # Platform is set for one test.
- 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):
- """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 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
- """
- self.parent = parent # Needed for Font call.
- self.message = message
- self.text0 = text0
- self.used_names = used_names
- Toplevel.__init__(self, parent)
- self.withdraw() # Hide while configuring, especially geometry.
- self.title(title)
- self.transient(parent)
- if not _utest: # Otherwise fail when directly run unittest.
- 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() # Need 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)
- ) )
- self.resizable(height=False, width=False)
- if not _utest:
- self.deiconify() # Unhide now that geometry set.
- self.entry.focus_set()
- self.wait_window()
- def create_widgets(self, ok_text='OK'): # 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.
- 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)
- # Display or blank error by setting ['text'] =.
- 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 # Override to add widgets.
- 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."
- 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.
- '''
- self.entry_error['text'] = ''
- 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()
- def destroy(self):
- self.grab_release()
- super().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."
- 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."
- 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.")
- return None
- except ImportError:
- # Some special modules require this (e.g. os.path)
- 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."
- # Used in editor.EditorWindow.goto_line_event.
- 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."
- # 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_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): # 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.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):
- """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
- def __init__(self, parent, title, *, cli_args=[],
- _htest=False, _utest=False):
- """cli_args is a list of strings.
- The list is assigned to the default Entry StringVar.
- The strings are displayed joined by ' ' for display.
- """
- 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):
- "Validity check and parsing for command line arguments."
- 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)
|