summaryrefslogtreecommitdiff
path: root/Lib/idlelib/query.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/idlelib/query.py')
-rw-r--r--Lib/idlelib/query.py308
1 files changed, 308 insertions, 0 deletions
diff --git a/Lib/idlelib/query.py b/Lib/idlelib/query.py
new file mode 100644
index 0000000000..3b1f1e25be
--- /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)