Issue #3068: Add Idle extension configuration dialog to Options menu.

Original patch by Tal Einat.
This commit is contained in:
Terry Jan Reedy 2014-10-22 20:15:18 -04:00
parent c803bd84b5
commit a9421fb3a3
6 changed files with 279 additions and 21 deletions

View File

@ -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', [

View File

@ -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:

View File

@ -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)
# Comment out Help button creation and packing until implement self.Help
## self.buttonHelp = Button(
## frame, 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
# Comment out Help button creation and packing until implement self.Help
## self.buttonHelp = Button(
## buttons, text='Help', command=self.Help,
## takefocus=FALSE, **paddingArgs)
## self.buttonHelp.pack(side=RIGHT, padx=5)
# 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)

View File

@ -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':

View File

@ -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',

View File

@ -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 = {}