|
20 | 20 | from idlelib.keybindingDialog import GetKeysDialog
|
21 | 21 | from idlelib.configSectionNameDialog import GetCfgSectionNameDialog
|
22 | 22 | from idlelib.configHelpSourceEdit import GetHelpSourceDialog
|
| 23 | +from idlelib.tabbedpages import TabbedPageSet |
23 | 24 | from idlelib import macosxSupport
|
24 |
| - |
25 | 25 | class ConfigDialog(Toplevel):
|
26 | 26 |
|
27 | 27 | def __init__(self, parent, title='', _htest=False, _utest=False):
|
@@ -85,36 +85,37 @@ def CreateWidgets(self):
|
85 | 85 | self.CreatePageKeys()
|
86 | 86 | self.CreatePageGeneral()
|
87 | 87 | self.create_action_buttons().pack(side=BOTTOM)
|
88 |
| - Frame(self, height=2, borderwidth=0).pack(side=BOTTOM) |
89 |
| - |
90 | 88 | def create_action_buttons(self):
|
91 | 89 | if macosxSupport.isAquaTk():
|
92 | 90 | # Changing the default padding on OSX results in unreadable
|
93 | 91 | # text in the buttons
|
94 | 92 | paddingArgs = {}
|
95 | 93 | else:
|
96 | 94 | paddingArgs = {'padx':6, 'pady':3}
|
97 |
| - |
98 |
| - frame = Frame(self, pady=2) |
| 95 | + outer = Frame(self, pady=2) |
| 96 | + buttons = Frame(outer, pady=2) |
99 | 97 | self.buttonOk = Button(
|
100 |
| - frame, text='Ok', command=self.Ok, |
| 98 | + buttons, text='Ok', command=self.Ok, |
101 | 99 | takefocus=FALSE, **paddingArgs)
|
102 | 100 | self.buttonApply = Button(
|
103 |
| - frame, text='Apply', command=self.Apply, |
| 101 | + buttons, text='Apply', command=self.Apply, |
104 | 102 | takefocus=FALSE, **paddingArgs)
|
105 | 103 | self.buttonCancel = Button(
|
106 |
| - frame, text='Cancel', command=self.Cancel, |
| 104 | + buttons, text='Cancel', command=self.Cancel, |
107 | 105 | 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) |
108 | 109 | # Comment out Help button creation and packing until implement self.Help
|
109 | 110 | ## self.buttonHelp = Button(
|
110 |
| -## frame, text='Help', command=self.Help, |
| 111 | +## buttons, text='Help', command=self.Help, |
111 | 112 | ## takefocus=FALSE, **paddingArgs)
|
112 | 113 | ## 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 |
117 | 114 |
|
| 115 | + # add space above buttons |
| 116 | + Frame(outer, height=2, borderwidth=0).pack(side=TOP) |
| 117 | + buttons.pack(side=BOTTOM) |
| 118 | + return outer |
118 | 119 | def CreatePageFontTab(self):
|
119 | 120 | parent = self.parent
|
120 | 121 | self.fontSize = StringVar(parent)
|
@@ -1188,10 +1189,252 @@ def Apply(self):
|
1188 | 1189 | def Help(self):
|
1189 | 1190 | pass
|
1190 | 1191 |
|
| 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 | + |
1191 | 1435 | if __name__ == '__main__':
|
1192 | 1436 | import unittest
|
1193 | 1437 | unittest.main('idlelib.idle_test.test_configdialog',
|
1194 | 1438 | verbosity=2, exit=False)
|
1195 |
| - |
1196 | 1439 | from idlelib.idle_test.htest import run
|
1197 |
| - run(ConfigDialog) |
| 1440 | + run(ConfigDialog, ConfigExtensionsDialog) |
0 commit comments