Skip to content

Commit fc759a6

Browse files
committed
Merge with 3.4 #3068
2 parents a5890d2 + c9d103c commit fc759a6

File tree

6 files changed

+278
-20
lines changed

6 files changed

+278
-20
lines changed

Lib/idlelib/Bindings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
('!_Auto-open Stack Viewer', '<<toggle-jit-stack-viewer>>'),
7878
]),
7979
('options', [
80-
('_Configure IDLE...', '<<open-config-dialog>>'),
80+
('Configure _IDLE', '<<open-config-dialog>>'),
81+
('Configure _Extensions', '<<open-config-extensions-dialog>>'),
8182
None,
8283
]),
8384
('help', [

Lib/idlelib/EditorWindow.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ def __init__(self, flist=None, filename=None, key=None, root=None):
189189
text.bind("<<python-docs>>", self.python_docs)
190190
text.bind("<<about-idle>>", self.about_dialog)
191191
text.bind("<<open-config-dialog>>", self.config_dialog)
192+
text.bind("<<open-config-extensions-dialog>>",
193+
self.config_extensions_dialog)
192194
text.bind("<<open-module>>", self.open_module)
193195
text.bind("<<do-nothing>>", lambda event: "break")
194196
text.bind("<<select-all>>", self.select_all)
@@ -543,6 +545,8 @@ def about_dialog(self, event=None):
543545

544546
def config_dialog(self, event=None):
545547
configDialog.ConfigDialog(self.top,'Settings')
548+
def config_extensions_dialog(self, event=None):
549+
configDialog.ConfigExtensionsDialog(self.top)
546550

547551
def help_dialog(self, event=None):
548552
if self.root:

Lib/idlelib/configDialog.py

Lines changed: 258 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from idlelib.keybindingDialog import GetKeysDialog
2121
from idlelib.configSectionNameDialog import GetCfgSectionNameDialog
2222
from idlelib.configHelpSourceEdit import GetHelpSourceDialog
23+
from idlelib.tabbedpages import TabbedPageSet
2324
from idlelib import macosxSupport
24-
2525
class ConfigDialog(Toplevel):
2626

2727
def __init__(self, parent, title='', _htest=False, _utest=False):
@@ -85,36 +85,37 @@ def CreateWidgets(self):
8585
self.CreatePageKeys()
8686
self.CreatePageGeneral()
8787
self.create_action_buttons().pack(side=BOTTOM)
88-
Frame(self, height=2, borderwidth=0).pack(side=BOTTOM)
89-
9088
def create_action_buttons(self):
9189
if macosxSupport.isAquaTk():
9290
# Changing the default padding on OSX results in unreadable
9391
# text in the buttons
9492
paddingArgs = {}
9593
else:
9694
paddingArgs = {'padx':6, 'pady':3}
97-
98-
frame = Frame(self, pady=2)
95+
outer = Frame(self, pady=2)
96+
buttons = Frame(outer, pady=2)
9997
self.buttonOk = Button(
100-
frame, text='Ok', command=self.Ok,
98+
buttons, text='Ok', command=self.Ok,
10199
takefocus=FALSE, **paddingArgs)
102100
self.buttonApply = Button(
103-
frame, text='Apply', command=self.Apply,
101+
buttons, text='Apply', command=self.Apply,
104102
takefocus=FALSE, **paddingArgs)
105103
self.buttonCancel = Button(
106-
frame, text='Cancel', command=self.Cancel,
104+
buttons, text='Cancel', command=self.Cancel,
107105
takefocus=FALSE, **paddingArgs)
106+
self.buttonOk.pack(side=LEFT, padx=5)
107+
self.buttonApply.pack(side=LEFT, padx=5)
108+
self.buttonCancel.pack(side=LEFT, padx=5)
108109
# Comment out Help button creation and packing until implement self.Help
109110
## self.buttonHelp = Button(
110-
## frame, text='Help', command=self.Help,
111+
## buttons, text='Help', command=self.Help,
111112
## takefocus=FALSE, **paddingArgs)
112113
## self.buttonHelp.pack(side=RIGHT, padx=5)
113-
self.buttonOk.pack(side=LEFT, padx=5)
114-
self.buttonApply.pack(side=LEFT, padx=5)
115-
self.buttonCancel.pack(side=LEFT, padx=5)
116-
return frame
117114

115+
# add space above buttons
116+
Frame(outer, height=2, borderwidth=0).pack(side=TOP)
117+
buttons.pack(side=BOTTOM)
118+
return outer
118119
def CreatePageFontTab(self):
119120
parent = self.parent
120121
self.fontSize = StringVar(parent)
@@ -1188,10 +1189,252 @@ def Apply(self):
11881189
def Help(self):
11891190
pass
11901191

1192+
class VerticalScrolledFrame(Frame):
1193+
"""A pure Tkinter vertically scrollable frame.
1194+
1195+
* Use the 'interior' attribute to place widgets inside the scrollable frame
1196+
* Construct and pack/place/grid normally
1197+
* This frame only allows vertical scrolling
1198+
"""
1199+
def __init__(self, parent, *args, **kw):
1200+
Frame.__init__(self, parent, *args, **kw)
1201+
1202+
# create a canvas object and a vertical scrollbar for scrolling it
1203+
vscrollbar = Scrollbar(self, orient=VERTICAL)
1204+
vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
1205+
canvas = Canvas(self, bd=0, highlightthickness=0,
1206+
yscrollcommand=vscrollbar.set)
1207+
canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
1208+
vscrollbar.config(command=canvas.yview)
1209+
1210+
# reset the view
1211+
canvas.xview_moveto(0)
1212+
canvas.yview_moveto(0)
1213+
1214+
# create a frame inside the canvas which will be scrolled with it
1215+
self.interior = interior = Frame(canvas)
1216+
interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
1217+
1218+
# track changes to the canvas and frame width and sync them,
1219+
# also updating the scrollbar
1220+
def _configure_interior(event):
1221+
# update the scrollbars to match the size of the inner frame
1222+
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
1223+
canvas.config(scrollregion="0 0 %s %s" % size)
1224+
if interior.winfo_reqwidth() != canvas.winfo_width():
1225+
# update the canvas's width to fit the inner frame
1226+
canvas.config(width=interior.winfo_reqwidth())
1227+
interior.bind('<Configure>', _configure_interior)
1228+
1229+
def _configure_canvas(event):
1230+
if interior.winfo_reqwidth() != canvas.winfo_width():
1231+
# update the inner frame's width to fill the canvas
1232+
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
1233+
canvas.bind('<Configure>', _configure_canvas)
1234+
1235+
return
1236+
1237+
def is_int(s):
1238+
"Return 's is blank or represents an int'"
1239+
if not s:
1240+
return True
1241+
try:
1242+
int(s)
1243+
return True
1244+
except ValueError:
1245+
return False
1246+
1247+
# TODO:
1248+
# * Revert to default(s)? Per option or per extension?
1249+
# * List options in their original order (possible??)
1250+
class ConfigExtensionsDialog(Toplevel):
1251+
"""A dialog for configuring IDLE extensions.
1252+
1253+
This dialog is generic - it works for any and all IDLE extensions.
1254+
1255+
IDLE extensions save their configuration options using idleConf.
1256+
ConfigExtensionsDialog reads the current configuration using idleConf,
1257+
supplies a GUI interface to change the configuration values, and saves the
1258+
changes using idleConf.
1259+
1260+
Not all changes take effect immediately - some may require restarting IDLE.
1261+
This depends on each extension's implementation.
1262+
1263+
All values are treated as text, and it is up to the user to supply
1264+
reasonable values. The only exception to this are the 'enable*' options,
1265+
which are boolean, and can be toggled with an True/False button.
1266+
"""
1267+
def __init__(self, parent, title=None, _htest=False):
1268+
Toplevel.__init__(self, parent)
1269+
self.wm_withdraw()
1270+
1271+
self.configure(borderwidth=5)
1272+
self.geometry(
1273+
"+%d+%d" % (parent.winfo_rootx() + 20,
1274+
parent.winfo_rooty() + (30 if not _htest else 150)))
1275+
self.wm_title(title or 'IDLE Extensions Configuration')
1276+
1277+
self.defaultCfg = idleConf.defaultCfg['extensions']
1278+
self.userCfg = idleConf.userCfg['extensions']
1279+
self.is_int = self.register(is_int)
1280+
self.load_extensions()
1281+
self.create_widgets()
1282+
1283+
self.resizable(height=FALSE, width=FALSE) # don't allow resizing yet
1284+
self.transient(parent)
1285+
self.protocol("WM_DELETE_WINDOW", self.Cancel)
1286+
self.tabbed_page_set.focus_set()
1287+
# wait for window to be generated
1288+
self.update()
1289+
# set current width as the minimum width
1290+
self.wm_minsize(self.winfo_width(), 1)
1291+
# now allow resizing
1292+
self.resizable(height=TRUE, width=TRUE)
1293+
1294+
self.wm_deiconify()
1295+
if not _htest:
1296+
self.grab_set()
1297+
self.wait_window()
1298+
1299+
def load_extensions(self):
1300+
"Fill self.extensions with data from the default and user configs."
1301+
self.extensions = {}
1302+
for ext_name in idleConf.GetExtensions(active_only=False):
1303+
self.extensions[ext_name] = []
1304+
1305+
for ext_name in self.extensions:
1306+
opt_list = sorted(self.defaultCfg.GetOptionList(ext_name))
1307+
1308+
# bring 'enable' options to the beginning of the list
1309+
enables = [opt_name for opt_name in opt_list
1310+
if opt_name.startswith('enable')]
1311+
for opt_name in enables:
1312+
opt_list.remove(opt_name)
1313+
opt_list = enables + opt_list
1314+
1315+
for opt_name in opt_list:
1316+
def_str = self.defaultCfg.Get(
1317+
ext_name, opt_name, raw=True)
1318+
try:
1319+
def_obj = {'True':True, 'False':False}[def_str]
1320+
opt_type = 'bool'
1321+
except KeyError:
1322+
try:
1323+
def_obj = int(def_str)
1324+
opt_type = 'int'
1325+
except ValueError:
1326+
def_obj = def_str
1327+
opt_type = None
1328+
try:
1329+
value = self.userCfg.Get(
1330+
ext_name, opt_name, type=opt_type, raw=True,
1331+
default=def_obj)
1332+
except ValueError: # Need this until .Get fixed
1333+
value = def_obj # bad values overwritten by entry
1334+
var = StringVar(self)
1335+
var.set(str(value))
1336+
1337+
self.extensions[ext_name].append({'name': opt_name,
1338+
'type': opt_type,
1339+
'default': def_str,
1340+
'value': value,
1341+
'var': var,
1342+
})
1343+
1344+
def create_widgets(self):
1345+
"""Create the dialog's widgets."""
1346+
self.rowconfigure(0, weight=1)
1347+
self.rowconfigure(1, weight=0)
1348+
self.columnconfigure(0, weight=1)
1349+
1350+
# create the tabbed pages
1351+
self.tabbed_page_set = TabbedPageSet(
1352+
self, page_names=self.extensions.keys(),
1353+
n_rows=None, max_tabs_per_row=5,
1354+
page_class=TabbedPageSet.PageRemove)
1355+
self.tabbed_page_set.grid(row=0, column=0, sticky=NSEW)
1356+
for ext_name in self.extensions:
1357+
self.create_tab_page(ext_name)
1358+
1359+
self.create_action_buttons().grid(row=1)
1360+
1361+
create_action_buttons = ConfigDialog.create_action_buttons
1362+
1363+
def create_tab_page(self, ext_name):
1364+
"""Create the page for an extension."""
1365+
1366+
page = LabelFrame(self.tabbed_page_set.pages[ext_name].frame,
1367+
border=2, padx=2, relief=GROOVE,
1368+
text=' %s ' % ext_name)
1369+
page.pack(fill=BOTH, expand=True, padx=12, pady=2)
1370+
1371+
# create the scrollable frame which will contain the entries
1372+
scrolled_frame = VerticalScrolledFrame(page, pady=2, height=250)
1373+
scrolled_frame.pack(side=BOTTOM, fill=BOTH, expand=TRUE)
1374+
entry_area = scrolled_frame.interior
1375+
entry_area.columnconfigure(0, weight=0)
1376+
entry_area.columnconfigure(1, weight=1)
1377+
1378+
# create an entry for each configuration option
1379+
for row, opt in enumerate(self.extensions[ext_name]):
1380+
# create a row with a label and entry/checkbutton
1381+
label = Label(entry_area, text=opt['name'])
1382+
label.grid(row=row, column=0, sticky=NW)
1383+
var = opt['var']
1384+
if opt['type'] == 'bool':
1385+
Checkbutton(entry_area, textvariable=var, variable=var,
1386+
onvalue='True', offvalue='False',
1387+
indicatoron=FALSE, selectcolor='', width=8
1388+
).grid(row=row, column=1, sticky=W, padx=7)
1389+
elif opt['type'] == 'int':
1390+
Entry(entry_area, textvariable=var, validate='key',
1391+
validatecommand=(self.is_int, '%P')
1392+
).grid(row=row, column=1, sticky=NSEW, padx=7)
1393+
1394+
else:
1395+
Entry(entry_area, textvariable=var
1396+
).grid(row=row, column=1, sticky=NSEW, padx=7)
1397+
return
1398+
1399+
1400+
Ok = ConfigDialog.Ok
1401+
1402+
def Apply(self):
1403+
self.save_all_changed_configs()
1404+
pass
1405+
1406+
Cancel = ConfigDialog.Cancel
1407+
1408+
def Help(self):
1409+
pass
1410+
1411+
def set_user_value(self, section, opt):
1412+
name = opt['name']
1413+
default = opt['default']
1414+
value = opt['var'].get().strip() or default
1415+
opt['var'].set(value)
1416+
# if self.defaultCfg.has_section(section):
1417+
# Currently, always true; if not, indent to return
1418+
if (value == default):
1419+
return self.userCfg.RemoveOption(section, name)
1420+
# set the option
1421+
return self.userCfg.SetOption(section, name, value)
1422+
1423+
def save_all_changed_configs(self):
1424+
"""Save configuration changes to the user config file."""
1425+
has_changes = False
1426+
for ext_name in self.extensions:
1427+
options = self.extensions[ext_name]
1428+
for opt in options:
1429+
if self.set_user_value(ext_name, opt):
1430+
has_changes = True
1431+
if has_changes:
1432+
self.userCfg.Save()
1433+
1434+
11911435
if __name__ == '__main__':
11921436
import unittest
11931437
unittest.main('idlelib.idle_test.test_configdialog',
11941438
verbosity=2, exit=False)
1195-
11961439
from idlelib.idle_test.htest import run
1197-
run(ConfigDialog)
1440+
run(ConfigDialog, ConfigExtensionsDialog)

Lib/idlelib/configHandler.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def Get(self, section, option, type=None, default=None, raw=False):
4444
Get an option value for given section/option or return default.
4545
If type is specified, return as type.
4646
"""
47+
# TODO Use default as fallback, at least if not None
48+
# Should also print Warning(file, section, option).
49+
# Currently may raise ValueError
4750
if not self.has_option(section, option):
4851
return default
4952
if type == 'bool':

Lib/idlelib/idle_test/htest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ def _wrapper(parent): # htest #
9393
"Double clicking on items prints a traceback for an exception "
9494
"that is ignored."
9595
}
96+
ConfigExtensionsDialog_spec = {
97+
'file': 'configDialog',
98+
'kwds': {'title': 'Test Extension Configuration',
99+
'_htest': True,},
100+
'msg': "IDLE extensions dialog.\n"
101+
"\n[Ok] to close the dialog.[Apply] to apply the settings and "
102+
"and [Cancel] to revert all changes.\nRe-run the test to ensure "
103+
"changes made have persisted."
104+
}
96105

97106
_color_delegator_spec = {
98107
'file': 'ColorDelegator',

Lib/idlelib/macosxSupport.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,9 @@ def overrideRootMenu(root, flist):
140140
# Remove the 'About' entry from the help menu, it is in the application
141141
# menu
142142
del Bindings.menudefs[-1][1][0:2]
143-
144-
# Remove the 'Configure' entry from the options menu, it is in the
143+
# Remove the 'Configure Idle' entry from the options menu, it is in the
145144
# application menu as 'Preferences'
146-
del Bindings.menudefs[-2][1][0:2]
147-
145+
del Bindings.menudefs[-2][1][0]
148146
menubar = Menu(root)
149147
root.configure(menu=menubar)
150148
menudict = {}

0 commit comments

Comments
 (0)