Issue #24782: Finish converting the Configure Extension dialog into a new

tab in the IDLE Preferences dialog.  Code patch by Mark Roseman.
This commit is contained in:
Terry Jan Reedy 2015-10-13 22:03:51 -04:00
parent 5805ddeedb
commit 93f3542ae4
6 changed files with 200 additions and 254 deletions

View File

@ -252,17 +252,16 @@ Options menu (Shell and Editor)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Configure IDLE Configure IDLE
Open a configuration dialog. Fonts, indentation, keybindings, and color Open a configuration dialog and change preferences for the following:
themes may be altered. Startup Preferences may be set, and additional fonts, indentation, keybindings, text color themes, startup windows and
help sources can be specified. Non-default user setting are saved in a size, additional help sources, and extensions (see below). On OS X,
.idlerc directory in the user's home directory. Problems caused by bad user open the configuration dialog by selecting Preferences in the application
configuration files are solved by editing or deleting one or more of the menu. To use a new built-in color theme (IDLE Dark) with older IDLEs,
files in .idlerc. On OS X, open the configuration dialog by selecting save it as a new custom theme.
Preferences in the application menu.
Configure Extensions Non-default user settings are saved in a .idlerc directory in the user's
Open a configuration dialog for setting preferences for extensions home directory. Problems caused by bad user configuration files are solved
(discussed below). See note above about the location of user settings. by editing or deleting one or more of the files in .idlerc.
Code Context (toggle)(Editor Window only) Code Context (toggle)(Editor Window only)
Open a pane at the top of the edit window which shows the block context Open a pane at the top of the edit window which shows the block context

View File

@ -78,7 +78,6 @@ menudefs = [
]), ]),
('options', [ ('options', [
('Configure _IDLE', '<<open-config-dialog>>'), ('Configure _IDLE', '<<open-config-dialog>>'),
('Configure _Extensions', '<<open-config-extensions-dialog>>'),
None, None,
]), ]),
('help', [ ('help', [

View File

@ -191,8 +191,6 @@ class EditorWindow(object):
text.bind("<<python-docs>>", self.python_docs) text.bind("<<python-docs>>", self.python_docs)
text.bind("<<about-idle>>", self.about_dialog) text.bind("<<about-idle>>", self.about_dialog)
text.bind("<<open-config-dialog>>", self.config_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("<<open-module>>", self.open_module)
text.bind("<<do-nothing>>", lambda event: "break") text.bind("<<do-nothing>>", lambda event: "break")
text.bind("<<select-all>>", self.select_all) text.bind("<<select-all>>", self.select_all)
@ -514,10 +512,6 @@ class EditorWindow(object):
# Synchronize with macosxSupport.overrideRootMenu.config_dialog. # Synchronize with macosxSupport.overrideRootMenu.config_dialog.
configDialog.ConfigDialog(self.top,'Settings') configDialog.ConfigDialog(self.top,'Settings')
def config_extensions_dialog(self, event=None):
"Handle Options 'Configure Extensions' event."
configDialog.ConfigExtensionsDialog(self.top)
def help_dialog(self, event=None): def help_dialog(self, event=None):
"Handle Help 'IDLE Help' event." "Handle Help 'IDLE Help' event."
# Synchronize with macosxSupport.overrideRootMenu.help_dialog. # Synchronize with macosxSupport.overrideRootMenu.help_dialog.

View File

@ -80,12 +80,14 @@ class ConfigDialog(Toplevel):
def CreateWidgets(self): def CreateWidgets(self):
self.tabPages = TabbedPageSet(self, self.tabPages = TabbedPageSet(self,
page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General']) page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General',
'Extensions'])
self.tabPages.pack(side=TOP, expand=TRUE, fill=BOTH) self.tabPages.pack(side=TOP, expand=TRUE, fill=BOTH)
self.CreatePageFontTab() self.CreatePageFontTab()
self.CreatePageHighlight() self.CreatePageHighlight()
self.CreatePageKeys() self.CreatePageKeys()
self.CreatePageGeneral() self.CreatePageGeneral()
self.CreatePageExtensions()
self.create_action_buttons().pack(side=BOTTOM) self.create_action_buttons().pack(side=BOTTOM)
def create_action_buttons(self): def create_action_buttons(self):
@ -1092,6 +1094,7 @@ class ConfigDialog(Toplevel):
self.LoadKeyCfg() self.LoadKeyCfg()
### general page ### general page
self.LoadGeneralCfg() self.LoadGeneralCfg()
# note: extension page handled separately
def SaveNewKeySet(self, keySetName, keySet): def SaveNewKeySet(self, keySetName, keySet):
""" """
@ -1145,6 +1148,7 @@ class ConfigDialog(Toplevel):
# save these even if unchanged! # save these even if unchanged!
idleConf.userCfg[configType].Save() idleConf.userCfg[configType].Save()
self.ResetChangedItems() #clear the changed items dict self.ResetChangedItems() #clear the changed items dict
self.save_all_changed_extensions() # uses a different mechanism
def DeactivateCurrentConfig(self): def DeactivateCurrentConfig(self):
#Before a config is saved, some cleanup of current #Before a config is saved, some cleanup of current
@ -1180,6 +1184,168 @@ class ConfigDialog(Toplevel):
view_text(self, title='Help for IDLE preferences', view_text(self, title='Help for IDLE preferences',
text=help_common+help_pages.get(page, '')) text=help_common+help_pages.get(page, ''))
def CreatePageExtensions(self):
"""Part of the config dialog used for configuring IDLE extensions.
This code is generic - it works for any and all IDLE extensions.
IDLE extensions save their configuration options using idleConf.
This code 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.
"""
parent = self.parent
frame = self.tabPages.pages['Extensions'].frame
self.ext_defaultCfg = idleConf.defaultCfg['extensions']
self.ext_userCfg = idleConf.userCfg['extensions']
self.is_int = self.register(is_int)
self.load_extensions()
# create widgets - a listbox shows all available extensions, with the
# controls for the extension selected in the listbox to the right
self.extension_names = StringVar(self)
frame.rowconfigure(0, weight=1)
frame.columnconfigure(2, weight=1)
self.extension_list = Listbox(frame, listvariable=self.extension_names,
selectmode='browse')
self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
scroll = Scrollbar(frame, command=self.extension_list.yview)
self.extension_list.yscrollcommand=scroll.set
self.details_frame = LabelFrame(frame, width=250, height=250)
self.extension_list.grid(column=0, row=0, sticky='nws')
scroll.grid(column=1, row=0, sticky='ns')
self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
frame.configure(padx=10, pady=10)
self.config_frame = {}
self.current_extension = None
self.outerframe = self # TEMPORARY
self.tabbed_page_set = self.extension_list # TEMPORARY
# create the frame holding controls for each extension
ext_names = ''
for ext_name in sorted(self.extensions):
self.create_extension_frame(ext_name)
ext_names = ext_names + '{' + ext_name + '} '
self.extension_names.set(ext_names)
self.extension_list.selection_set(0)
self.extension_selected(None)
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.ext_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.ext_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.ext_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 extension_selected(self, event):
newsel = self.extension_list.curselection()
if newsel:
newsel = self.extension_list.get(newsel)
if newsel is None or newsel != self.current_extension:
if self.current_extension:
self.details_frame.config(text='')
self.config_frame[self.current_extension].grid_forget()
self.current_extension = None
if newsel:
self.details_frame.config(text=newsel)
self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
self.current_extension = newsel
def create_extension_frame(self, ext_name):
"""Create a frame holding the widgets to configure one extension"""
f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
self.config_frame[ext_name] = f
entry_area = f.interior
# 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
def set_extension_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.ext_userCfg.RemoveOption(section, name)
# set the option
return self.ext_userCfg.SetOption(section, name, value)
def save_all_changed_extensions(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_extension_value(ext_name, opt):
has_changes = True
if has_changes:
self.ext_userCfg.Save()
help_common = '''\ help_common = '''\
When you click either the Apply or Ok buttons, settings in this When you click either the Apply or Ok buttons, settings in this
dialog that are different from IDLE's default are saved in dialog that are different from IDLE's default are saved in
@ -1198,6 +1364,17 @@ theme, with a different name.
} }
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
class VerticalScrolledFrame(Frame): class VerticalScrolledFrame(Frame):
"""A pure Tkinter vertically scrollable frame. """A pure Tkinter vertically scrollable frame.
@ -1240,221 +1417,6 @@ class VerticalScrolledFrame(Frame):
return 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.extension_names = StringVar(self)
self.rowconfigure(0, weight=1)
self.columnconfigure(2, weight=1)
self.extension_list = Listbox(self, listvariable=self.extension_names,
selectmode='browse')
self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
scroll = Scrollbar(self, command=self.extension_list.yview)
self.extension_list.yscrollcommand=scroll.set
self.details_frame = LabelFrame(self, width=250, height=250)
self.extension_list.grid(column=0, row=0, sticky='nws')
scroll.grid(column=1, row=0, sticky='ns')
self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
self.configure(padx=10, pady=10)
self.config_frame = {}
self.current_extension = None
self.outerframe = self # TEMPORARY
self.tabbed_page_set = self.extension_list # TEMPORARY
# create the individual pages
ext_names = ''
for ext_name in sorted(self.extensions):
self.create_extension_frame(ext_name)
ext_names = ext_names + '{' + ext_name + '} '
self.extension_names.set(ext_names)
self.extension_list.selection_set(0)
self.extension_selected(None)
self.create_action_buttons().grid(row=1, columnspan=3)
def extension_selected(self, event):
newsel = self.extension_list.curselection()
if newsel:
newsel = self.extension_list.get(newsel)
if newsel is None or newsel != self.current_extension:
if self.current_extension:
self.details_frame.config(text='')
self.config_frame[self.current_extension].grid_forget()
self.current_extension = None
if newsel:
self.details_frame.config(text=newsel)
self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
self.current_extension = newsel
create_action_buttons = ConfigDialog.create_action_buttons
def create_extension_frame(self, ext_name):
"""Create a frame holding the widgets to configure one extension"""
f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
self.config_frame[ext_name] = f
entry_area = f.interior
# 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__': if __name__ == '__main__':
import unittest import unittest

View File

@ -266,16 +266,16 @@ access to locals and globals.</dd>
<h3>25.5.1.7. Options menu (Shell and Editor)<a class="headerlink" href="#options-menu-shell-and-editor" title="Permalink to this headline"></a></h3> <h3>25.5.1.7. Options menu (Shell and Editor)<a class="headerlink" href="#options-menu-shell-and-editor" title="Permalink to this headline"></a></h3>
<dl class="docutils"> <dl class="docutils">
<dt>Configure IDLE</dt> <dt>Configure IDLE</dt>
<dd>Open a configuration dialog. Fonts, indentation, keybindings, and color <dd><p class="first">Open a configuration dialog and change preferences for the following:
themes may be altered. Startup Preferences may be set, and additional fonts, indentation, keybindings, text color themes, startup windows and
help sources can be specified. Non-default user setting are saved in a size, additional help sources, and extensions (see below). On OS X,
.idlerc directory in the user&#8217;s home directory. Problems caused by bad user open the configuration dialog by selecting Preferences in the application
configuration files are solved by editing or deleting one or more of the menu. To use a new built-in color theme (IDLE Dark) with older IDLEs,
files in .idlerc. On OS X, open the configuration dialog by selecting save it as a new custom theme.</p>
Preferences in the application menu.</dd> <p class="last">Non-default user settings are saved in a .idlerc directory in the user&#8217;s
<dt>Configure Extensions</dt> home directory. Problems caused by bad user configuration files are solved
<dd>Open a configuration dialog for setting preferences for extensions by editing or deleting one or more of the files in .idlerc.</p>
(discussed below). See note above about the location of user settings.</dd> </dd>
<dt>Code Context (toggle)(Editor Window only)</dt> <dt>Code Context (toggle)(Editor Window only)</dt>
<dd>Open a pane at the top of the edit window which shows the block context <dd>Open a pane at the top of the edit window which shows the block context
of the code which has scrolled above the top of the window.</dd> of the code which has scrolled above the top of the window.</dd>
@ -699,7 +699,7 @@ are currently:</p>
The Python Software Foundation is a non-profit corporation. The Python Software Foundation is a non-profit corporation.
<a href="https://www.python.org/psf/donations/">Please donate.</a> <a href="https://www.python.org/psf/donations/">Please donate.</a>
<br /> <br />
Last updated on Oct 02, 2015. Last updated on Oct 13, 2015.
<a href="../bugs.html">Found a bug</a>? <a href="../bugs.html">Found a bug</a>?
<br /> <br />
Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> 1.2.3. Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> 1.2.3.

View File

@ -93,15 +93,6 @@ _class_browser_spec = {
"Double clicking on items prints a traceback for an exception " "Double clicking on items prints a traceback for an exception "
"that is ignored." "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 = { _color_delegator_spec = {
'file': 'ColorDelegator', 'file': 'ColorDelegator',
@ -121,7 +112,8 @@ ConfigDialog_spec = {
"font face of the text in the area below it.\nIn the " "font face of the text in the area below it.\nIn the "
"'Highlighting' tab, try different color schemes. Clicking " "'Highlighting' tab, try different color schemes. Clicking "
"items in the sample program should update the choices above it." "items in the sample program should update the choices above it."
"\nIn the 'Keys' and 'General' tab, test settings of interest." "\nIn the 'Keys', 'General' and 'Extensions' tabs, test settings"
"of interest."
"\n[Ok] to close the dialog.[Apply] to apply the settings and " "\n[Ok] to close the dialog.[Apply] to apply the settings and "
"and [Cancel] to revert all changes.\nRe-run the test to ensure " "and [Cancel] to revert all changes.\nRe-run the test to ensure "
"changes made have persisted." "changes made have persisted."