summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTerry Jan Reedy <tjreedy@udel.edu>2014-10-23 00:15:18 (GMT)
committerTerry Jan Reedy <tjreedy@udel.edu>2014-10-23 00:15:18 (GMT)
commita9421fb3a3013a8aacc18959c28c1b0002f34025 (patch)
tree21947d5899838ca1deab5d84fcfb3a40abd226b8
parentc803bd84b529f3a300f9ba808b60dd50e4e365bf (diff)
downloadcpython-a9421fb3a3013a8aacc18959c28c1b0002f34025.zip
cpython-a9421fb3a3013a8aacc18959c28c1b0002f34025.tar.gz
cpython-a9421fb3a3013a8aacc18959c28c1b0002f34025.tar.bz2
Issue #3068: Add Idle extension configuration dialog to Options menu.
Original patch by Tal Einat.
-rw-r--r--Lib/idlelib/Bindings.py3
-rw-r--r--Lib/idlelib/EditorWindow.py4
-rw-r--r--Lib/idlelib/configDialog.py273
-rw-r--r--Lib/idlelib/configHandler.py3
-rw-r--r--Lib/idlelib/idle_test/htest.py9
-rw-r--r--Lib/idlelib/macosxSupport.py6
6 files changed, 278 insertions, 20 deletions
diff --git a/Lib/idlelib/Bindings.py b/Lib/idlelib/Bindings.py
index c9bef21..226671c 100644
--- a/Lib/idlelib/Bindings.py
+++ b/Lib/idlelib/Bindings.py
@@ -77,7 +77,8 @@ menudefs = [
('!_Auto-open Stack Viewer', '<<toggle-jit-stack-viewer>>'),
]),
('options', [
- ('_Configure IDLE...', '<<open-config-dialog>>'),
+ ('Configure _IDLE', '<<open-config-dialog>>'),
+ ('Configure _Extensions', '<<open-config-extensions-dialog>>'),
None,
]),
('help', [
diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py
index bdf4a1a..7d2ea8f 100644
--- a/Lib/idlelib/EditorWindow.py
+++ b/Lib/idlelib/EditorWindow.py
@@ -189,6 +189,8 @@ class EditorWindow(object):
text.bind("<<python-docs>>", self.python_docs)
text.bind("<<about-idle>>", self.about_dialog)
text.bind("<<open-config-dialog>>", self.config_dialog)
+ text.bind("<<open-config-extensions-dialog>>",
+ self.config_extensions_dialog)
text.bind("<<open-module>>", self.open_module)
text.bind("<<do-nothing>>", lambda event: "break")
text.bind("<<select-all>>", self.select_all)
@@ -543,6 +545,8 @@ class EditorWindow(object):
def config_dialog(self, event=None):
configDialog.ConfigDialog(self.top,'Settings')
+ def config_extensions_dialog(self, event=None):
+ configDialog.ConfigExtensionsDialog(self.top)
def help_dialog(self, event=None):
if self.root:
diff --git a/Lib/idlelib/configDialog.py b/Lib/idlelib/configDialog.py
index 938b0bc..57565ae 100644
--- a/Lib/idlelib/configDialog.py
+++ b/Lib/idlelib/configDialog.py
@@ -20,8 +20,8 @@ from idlelib.tabbedpages import TabbedPageSet
from idlelib.keybindingDialog import GetKeysDialog
from idlelib.configSectionNameDialog import GetCfgSectionNameDialog
from idlelib.configHelpSourceEdit import GetHelpSourceDialog
+from idlelib.tabbedpages import TabbedPageSet
from idlelib import macosxSupport
-
class ConfigDialog(Toplevel):
def __init__(self, parent, title='', _htest=False, _utest=False):
@@ -85,8 +85,6 @@ class ConfigDialog(Toplevel):
self.CreatePageKeys()
self.CreatePageGeneral()
self.create_action_buttons().pack(side=BOTTOM)
- Frame(self, height=2, borderwidth=0).pack(side=BOTTOM)
-
def create_action_buttons(self):
if macosxSupport.isAquaTk():
# Changing the default padding on OSX results in unreadable
@@ -94,27 +92,30 @@ class ConfigDialog(Toplevel):
paddingArgs = {}
else:
paddingArgs = {'padx':6, 'pady':3}
-
- frame = Frame(self, pady=2)
+ outer = Frame(self, pady=2)
+ buttons = Frame(outer, pady=2)
self.buttonOk = Button(
- frame, text='Ok', command=self.Ok,
+ buttons, text='Ok', command=self.Ok,
takefocus=FALSE, **paddingArgs)
self.buttonApply = Button(
- frame, text='Apply', command=self.Apply,
+ buttons, text='Apply', command=self.Apply,
takefocus=FALSE, **paddingArgs)
self.buttonCancel = Button(
- frame, text='Cancel', command=self.Cancel,
+ buttons, text='Cancel', command=self.Cancel,
takefocus=FALSE, **paddingArgs)
+ self.buttonOk.pack(side=LEFT, padx=5)
+ self.buttonApply.pack(side=LEFT, padx=5)
+ self.buttonCancel.pack(side=LEFT, padx=5)
# Comment out Help button creation and packing until implement self.Help
## self.buttonHelp = Button(
-## frame, text='Help', command=self.Help,
+## buttons, text='Help', command=self.Help,
## takefocus=FALSE, **paddingArgs)
## self.buttonHelp.pack(side=RIGHT, padx=5)
- self.buttonOk.pack(side=LEFT, padx=5)
- self.buttonApply.pack(side=LEFT, padx=5)
- self.buttonCancel.pack(side=LEFT, padx=5)
- return frame
+ # add space above buttons
+ Frame(outer, height=2, borderwidth=0).pack(side=TOP)
+ buttons.pack(side=BOTTOM)
+ return outer
def CreatePageFontTab(self):
parent = self.parent
self.fontSize = StringVar(parent)
@@ -1188,10 +1189,252 @@ class ConfigDialog(Toplevel):
def Help(self):
pass
+class VerticalScrolledFrame(Frame):
+ """A pure Tkinter vertically scrollable frame.
+
+ * Use the 'interior' attribute to place widgets inside the scrollable frame
+ * Construct and pack/place/grid normally
+ * This frame only allows vertical scrolling
+ """
+ def __init__(self, parent, *args, **kw):
+ Frame.__init__(self, parent, *args, **kw)
+
+ # create a canvas object and a vertical scrollbar for scrolling it
+ vscrollbar = Scrollbar(self, orient=VERTICAL)
+ vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
+ canvas = Canvas(self, bd=0, highlightthickness=0,
+ yscrollcommand=vscrollbar.set)
+ canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
+ vscrollbar.config(command=canvas.yview)
+
+ # reset the view
+ canvas.xview_moveto(0)
+ canvas.yview_moveto(0)
+
+ # create a frame inside the canvas which will be scrolled with it
+ self.interior = interior = Frame(canvas)
+ interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
+
+ # track changes to the canvas and frame width and sync them,
+ # also updating the scrollbar
+ def _configure_interior(event):
+ # update the scrollbars to match the size of the inner frame
+ size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
+ canvas.config(scrollregion="0 0 %s %s" % size)
+ if interior.winfo_reqwidth() != canvas.winfo_width():
+ # update the canvas's width to fit the inner frame
+ canvas.config(width=interior.winfo_reqwidth())
+ interior.bind('<Configure>', _configure_interior)
+
+ def _configure_canvas(event):
+ if interior.winfo_reqwidth() != canvas.winfo_width():
+ # update the inner frame's width to fill the canvas
+ canvas.itemconfigure(interior_id, width=canvas.winfo_width())
+ canvas.bind('<Configure>', _configure_canvas)
+
+ return
+
+def is_int(s):
+ "Return 's is blank or represents an int'"
+ if not s:
+ return True
+ try:
+ int(s)
+ return True
+ except ValueError:
+ return False
+
+# TODO:
+# * Revert to default(s)? Per option or per extension?
+# * List options in their original order (possible??)
+class ConfigExtensionsDialog(Toplevel):
+ """A dialog for configuring IDLE extensions.
+
+ This dialog is generic - it works for any and all IDLE extensions.
+
+ IDLE extensions save their configuration options using idleConf.
+ ConfigExtensionsDialog reads the current configuration using idleConf,
+ supplies a GUI interface to change the configuration values, and saves the
+ changes using idleConf.
+
+ Not all changes take effect immediately - some may require restarting IDLE.
+ This depends on each extension's implementation.
+
+ All values are treated as text, and it is up to the user to supply
+ reasonable values. The only exception to this are the 'enable*' options,
+ which are boolean, and can be toggled with an True/False button.
+ """
+ def __init__(self, parent, title=None, _htest=False):
+ Toplevel.__init__(self, parent)
+ self.wm_withdraw()
+
+ self.configure(borderwidth=5)
+ self.geometry(
+ "+%d+%d" % (parent.winfo_rootx() + 20,
+ parent.winfo_rooty() + (30 if not _htest else 150)))
+ self.wm_title(title or 'IDLE Extensions Configuration')
+
+ self.defaultCfg = idleConf.defaultCfg['extensions']
+ self.userCfg = idleConf.userCfg['extensions']
+ self.is_int = self.register(is_int)
+ self.load_extensions()
+ self.create_widgets()
+
+ self.resizable(height=FALSE, width=FALSE) # don't allow resizing yet
+ self.transient(parent)
+ self.protocol("WM_DELETE_WINDOW", self.Cancel)
+ self.tabbed_page_set.focus_set()
+ # wait for window to be generated
+ self.update()
+ # set current width as the minimum width
+ self.wm_minsize(self.winfo_width(), 1)
+ # now allow resizing
+ self.resizable(height=TRUE, width=TRUE)
+
+ self.wm_deiconify()
+ if not _htest:
+ self.grab_set()
+ self.wait_window()
+
+ def load_extensions(self):
+ "Fill self.extensions with data from the default and user configs."
+ self.extensions = {}
+ for ext_name in idleConf.GetExtensions(active_only=False):
+ self.extensions[ext_name] = []
+
+ for ext_name in self.extensions:
+ opt_list = sorted(self.defaultCfg.GetOptionList(ext_name))
+
+ # bring 'enable' options to the beginning of the list
+ enables = [opt_name for opt_name in opt_list
+ if opt_name.startswith('enable')]
+ for opt_name in enables:
+ opt_list.remove(opt_name)
+ opt_list = enables + opt_list
+
+ for opt_name in opt_list:
+ def_str = self.defaultCfg.Get(
+ ext_name, opt_name, raw=True)
+ try:
+ def_obj = {'True':True, 'False':False}[def_str]
+ opt_type = 'bool'
+ except KeyError:
+ try:
+ def_obj = int(def_str)
+ opt_type = 'int'
+ except ValueError:
+ def_obj = def_str
+ opt_type = None
+ try:
+ value = self.userCfg.Get(
+ ext_name, opt_name, type=opt_type, raw=True,
+ default=def_obj)
+ except ValueError: # Need this until .Get fixed
+ value = def_obj # bad values overwritten by entry
+ var = StringVar(self)
+ var.set(str(value))
+
+ self.extensions[ext_name].append({'name': opt_name,
+ 'type': opt_type,
+ 'default': def_str,
+ 'value': value,
+ 'var': var,
+ })
+
+ def create_widgets(self):
+ """Create the dialog's widgets."""
+ self.rowconfigure(0, weight=1)
+ self.rowconfigure(1, weight=0)
+ self.columnconfigure(0, weight=1)
+
+ # create the tabbed pages
+ self.tabbed_page_set = TabbedPageSet(
+ self, page_names=self.extensions.keys(),
+ n_rows=None, max_tabs_per_row=5,
+ page_class=TabbedPageSet.PageRemove)
+ self.tabbed_page_set.grid(row=0, column=0, sticky=NSEW)
+ for ext_name in self.extensions:
+ self.create_tab_page(ext_name)
+
+ self.create_action_buttons().grid(row=1)
+
+ create_action_buttons = ConfigDialog.create_action_buttons
+
+ def create_tab_page(self, ext_name):
+ """Create the page for an extension."""
+
+ page = LabelFrame(self.tabbed_page_set.pages[ext_name].frame,
+ border=2, padx=2, relief=GROOVE,
+ text=' %s ' % ext_name)
+ page.pack(fill=BOTH, expand=True, padx=12, pady=2)
+
+ # create the scrollable frame which will contain the entries
+ scrolled_frame = VerticalScrolledFrame(page, pady=2, height=250)
+ scrolled_frame.pack(side=BOTTOM, fill=BOTH, expand=TRUE)
+ entry_area = scrolled_frame.interior
+ entry_area.columnconfigure(0, weight=0)
+ entry_area.columnconfigure(1, weight=1)
+
+ # create an entry for each configuration option
+ for row, opt in enumerate(self.extensions[ext_name]):
+ # create a row with a label and entry/checkbutton
+ label = Label(entry_area, text=opt['name'])
+ label.grid(row=row, column=0, sticky=NW)
+ var = opt['var']
+ if opt['type'] == 'bool':
+ Checkbutton(entry_area, textvariable=var, variable=var,
+ onvalue='True', offvalue='False',
+ indicatoron=FALSE, selectcolor='', width=8
+ ).grid(row=row, column=1, sticky=W, padx=7)
+ elif opt['type'] == 'int':
+ Entry(entry_area, textvariable=var, validate='key',
+ validatecommand=(self.is_int, '%P')
+ ).grid(row=row, column=1, sticky=NSEW, padx=7)
+
+ else:
+ Entry(entry_area, textvariable=var
+ ).grid(row=row, column=1, sticky=NSEW, padx=7)
+ return
+
+
+ Ok = ConfigDialog.Ok
+
+ def Apply(self):
+ self.save_all_changed_configs()
+ pass
+
+ Cancel = ConfigDialog.Cancel
+
+ def Help(self):
+ pass
+
+ def set_user_value(self, section, opt):
+ name = opt['name']
+ default = opt['default']
+ value = opt['var'].get().strip() or default
+ opt['var'].set(value)
+ # if self.defaultCfg.has_section(section):
+ # Currently, always true; if not, indent to return
+ if (value == default):
+ return self.userCfg.RemoveOption(section, name)
+ # set the option
+ return self.userCfg.SetOption(section, name, value)
+
+ def save_all_changed_configs(self):
+ """Save configuration changes to the user config file."""
+ has_changes = False
+ for ext_name in self.extensions:
+ options = self.extensions[ext_name]
+ for opt in options:
+ if self.set_user_value(ext_name, opt):
+ has_changes = True
+ if has_changes:
+ self.userCfg.Save()
+
+
if __name__ == '__main__':
import unittest
unittest.main('idlelib.idle_test.test_configdialog',
verbosity=2, exit=False)
-
from idlelib.idle_test.htest import run
- run(ConfigDialog)
+ run(ConfigDialog, ConfigExtensionsDialog)
diff --git a/Lib/idlelib/configHandler.py b/Lib/idlelib/configHandler.py
index 7a282fb..b94b8f1 100644
--- a/Lib/idlelib/configHandler.py
+++ b/Lib/idlelib/configHandler.py
@@ -44,6 +44,9 @@ class IdleConfParser(ConfigParser):
Get an option value for given section/option or return default.
If type is specified, return as type.
"""
+ # TODO Use default as fallback, at least if not None
+ # Should also print Warning(file, section, option).
+ # Currently may raise ValueError
if not self.has_option(section, option):
return default
if type == 'bool':
diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py
index 1fe5ad4..aa7f2e8 100644
--- a/Lib/idlelib/idle_test/htest.py
+++ b/Lib/idlelib/idle_test/htest.py
@@ -93,6 +93,15 @@ _class_browser_spec = {
"Double clicking on items prints a traceback for an exception "
"that is ignored."
}
+ConfigExtensionsDialog_spec = {
+ 'file': 'configDialog',
+ 'kwds': {'title': 'Test Extension Configuration',
+ '_htest': True,},
+ 'msg': "IDLE extensions dialog.\n"
+ "\n[Ok] to close the dialog.[Apply] to apply the settings and "
+ "and [Cancel] to revert all changes.\nRe-run the test to ensure "
+ "changes made have persisted."
+ }
_color_delegator_spec = {
'file': 'ColorDelegator',
diff --git a/Lib/idlelib/macosxSupport.py b/Lib/idlelib/macosxSupport.py
index b6488f8..65bd688 100644
--- a/Lib/idlelib/macosxSupport.py
+++ b/Lib/idlelib/macosxSupport.py
@@ -140,11 +140,9 @@ def overrideRootMenu(root, flist):
# Remove the 'About' entry from the help menu, it is in the application
# menu
del Bindings.menudefs[-1][1][0:2]
-
- # Remove the 'Configure' entry from the options menu, it is in the
+ # Remove the 'Configure Idle' entry from the options menu, it is in the
# application menu as 'Preferences'
- del Bindings.menudefs[-2][1][0:2]
-
+ del Bindings.menudefs[-2][1][0]
menubar = Menu(root)
root.configure(menu=menubar)
menudict = {}