Coverage for configdialog.py: 11%
1029 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1"""IDLE Configuration Dialog: support user customization of IDLE by GUI
3Customize font faces, sizes, and colorization attributes. Set indentation
4defaults. Customize keybindings. Colorization and keybindings can be
5saved as user defined sets. Select startup options including shell/editor
6and default window size. Define additional help sources.
8Note that tab width in IDLE is currently fixed at eight due to Tk issues.
9Refer to comments in EditorWindow autoindent code for details.
11"""
12import re
14from tkinter import (Toplevel, Listbox, Canvas,
15 StringVar, BooleanVar, IntVar, TRUE, FALSE,
16 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE,
17 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
18 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END, TclError)
19from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label,
20 OptionMenu, Notebook, Radiobutton, Scrollbar, Style,
21 Spinbox, Combobox)
22from tkinter import colorchooser
23import tkinter.font as tkfont
24from tkinter import messagebox
26from idlelib.config import idleConf, ConfigChanges
27from idlelib.config_key import GetKeysWindow
28from idlelib.dynoption import DynOptionMenu
29from idlelib import macosx
30from idlelib.query import SectionName, HelpSource
31from idlelib.textview import view_text
32from idlelib.autocomplete import AutoComplete
33from idlelib.codecontext import CodeContext
34from idlelib.parenmatch import ParenMatch
35from idlelib.format import FormatParagraph
36from idlelib.squeezer import Squeezer
37from idlelib.textview import ScrollableTextFrame
39changes = ConfigChanges()
40# Reload changed options in the following classes.
41reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
42 Squeezer)
45class ConfigDialog(Toplevel):
46 """Config dialog for IDLE.
47 """
49 def __init__(self, parent, title='', *, _htest=False, _utest=False):
50 """Show the tabbed dialog for user configuration.
52 Args:
53 parent - parent of this dialog
54 title - string which is the title of this popup dialog
55 _htest - bool, change box location when running htest
56 _utest - bool, don't wait_window when running unittest
58 Note: Focus set on font page fontlist.
60 Methods:
61 create_widgets
62 cancel: Bound to DELETE_WINDOW protocol.
63 """
64 Toplevel.__init__(self, parent)
65 self.parent = parent
66 if _htest:
67 parent.instance_dict = {}
68 if not _utest:
69 self.withdraw()
71 self.title(title or 'IDLE Preferences')
72 x = parent.winfo_rootx() + 20
73 y = parent.winfo_rooty() + (30 if not _htest else 150)
74 self.geometry(f'+{x}+{y}')
75 # Each theme element key is its display name.
76 # The first value of the tuple is the sample area tag name.
77 # The second value is the display name list sort index.
78 self.create_widgets()
79 self.resizable(height=FALSE, width=FALSE)
80 self.transient(parent)
81 self.protocol("WM_DELETE_WINDOW", self.cancel)
82 self.fontpage.fontlist.focus_set()
83 # XXX Decide whether to keep or delete these key bindings.
84 # Key bindings for this dialog.
85 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
86 # self.bind('<Alt-a>', self.Apply) #apply changes, save
87 # self.bind('<F1>', self.Help) #context help
88 # Attach callbacks after loading config to avoid calling them.
89 tracers.attach()
91 if not _utest:
92 self.grab_set()
93 self.wm_deiconify()
94 self.wait_window()
96 def create_widgets(self):
97 """Create and place widgets for tabbed dialog.
99 Widgets Bound to self:
100 frame: encloses all other widgets
101 note: Notebook
102 highpage: HighPage
103 fontpage: FontPage
104 keyspage: KeysPage
105 winpage: WinPage
106 shedpage: ShedPage
107 extpage: ExtPage
109 Methods:
110 create_action_buttons
111 load_configs: Load pages except for extensions.
112 activate_config_changes: Tell editors to reload.
113 """
114 self.frame = frame = Frame(self, padding="5px")
115 self.frame.grid(sticky="nwes")
116 self.note = note = Notebook(frame)
117 self.extpage = ExtPage(note)
118 self.highpage = HighPage(note, self.extpage)
119 self.fontpage = FontPage(note, self.highpage)
120 self.keyspage = KeysPage(note, self.extpage)
121 self.winpage = WinPage(note)
122 self.shedpage = ShedPage(note)
124 note.add(self.fontpage, text=' Fonts ')
125 note.add(self.highpage, text='Highlights')
126 note.add(self.keyspage, text=' Keys ')
127 note.add(self.winpage, text=' Windows ')
128 note.add(self.shedpage, text=' Shell/Ed ')
129 note.add(self.extpage, text='Extensions')
130 note.enable_traversal()
131 note.pack(side=TOP, expand=TRUE, fill=BOTH)
132 self.create_action_buttons().pack(side=BOTTOM)
134 def create_action_buttons(self):
135 """Return frame of action buttons for dialog.
137 Methods:
138 ok
139 apply
140 cancel
141 help
143 Widget Structure:
144 outer: Frame
145 buttons: Frame
146 (no assignment): Button (ok)
147 (no assignment): Button (apply)
148 (no assignment): Button (cancel)
149 (no assignment): Button (help)
150 (no assignment): Frame
151 """
152 if macosx.isAquaTk():
153 # Changing the default padding on OSX results in unreadable
154 # text in the buttons.
155 padding_args = {}
156 else:
157 padding_args = {'padding': (6, 3)}
158 outer = Frame(self.frame, padding=2)
159 buttons_frame = Frame(outer, padding=2)
160 self.buttons = {}
161 for txt, cmd in (
162 ('Ok', self.ok),
163 ('Apply', self.apply),
164 ('Cancel', self.cancel),
165 ('Help', self.help)):
166 self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd,
167 takefocus=FALSE, **padding_args)
168 self.buttons[txt].pack(side=LEFT, padx=5)
169 # Add space above buttons.
170 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
171 buttons_frame.pack(side=BOTTOM)
172 return outer
174 def ok(self):
175 """Apply config changes, then dismiss dialog."""
176 self.apply()
177 self.destroy()
179 def apply(self):
180 """Apply config changes and leave dialog open."""
181 self.deactivate_current_config()
182 changes.save_all()
183 self.extpage.save_all_changed_extensions()
184 self.activate_config_changes()
186 def cancel(self):
187 """Dismiss config dialog.
189 Methods:
190 destroy: inherited
191 """
192 changes.clear()
193 self.destroy()
195 def destroy(self):
196 global font_sample_text
197 font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
198 self.grab_release()
199 super().destroy()
201 def help(self):
202 """Create textview for config dialog help.
204 Attributes accessed:
205 note
206 Methods:
207 view_text: Method from textview module.
208 """
209 page = self.note.tab(self.note.select(), option='text').strip()
210 view_text(self, title='Help for IDLE preferences',
211 contents=help_common+help_pages.get(page, ''))
213 def deactivate_current_config(self):
214 """Remove current key bindings.
215 Iterate over window instances defined in parent and remove
216 the keybindings.
217 """
218 # Before a config is saved, some cleanup of current
219 # config must be done - remove the previous keybindings.
220 win_instances = self.parent.instance_dict.keys()
221 for instance in win_instances:
222 instance.RemoveKeybindings()
224 def activate_config_changes(self):
225 """Apply configuration changes to current windows.
227 Dynamically update the current parent window instances
228 with some of the configuration changes.
229 """
230 win_instances = self.parent.instance_dict.keys()
231 for instance in win_instances:
232 instance.ResetColorizer()
233 instance.ResetFont()
234 instance.set_notabs_indentwidth()
235 instance.ApplyKeybindings()
236 instance.reset_help_menu_entries()
237 instance.update_cursor_blink()
238 for klass in reloadables:
239 klass.reload()
242# class TabPage(Frame): # A template for Page classes.
243# def __init__(self, master):
244# super().__init__(master)
245# self.create_page_tab()
246# self.load_tab_cfg()
247# def create_page_tab(self):
248# # Define tk vars and register var and callback with tracers.
249# # Create subframes and widgets.
250# # Pack widgets.
251# def load_tab_cfg(self):
252# # Initialize widgets with data from idleConf.
253# def var_changed_var_name():
254# # For each tk var that needs other than default callback.
255# def other_methods():
256# # Define tab-specific behavior.
258font_sample_text = (
259 '<ASCII/Latin1>\n'
260 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
261 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
262 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
263 '\n<IPA,Greek,Cyrillic>\n'
264 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
265 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
266 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
267 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
268 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
269 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
270 '\n<Hebrew, Arabic>\n'
271 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
272 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
273 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
274 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
275 '\n<Devanagari, Tamil>\n'
276 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
277 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
278 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
279 '\u0b85\u0b87\u0b89\u0b8e\n'
280 '\n<East Asian>\n'
281 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
282 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
283 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
284 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
285 )
288class FontPage(Frame):
290 def __init__(self, master, highpage):
291 super().__init__(master)
292 self.highlight_sample = highpage.highlight_sample
293 self.create_page_font()
294 self.load_font_cfg()
296 def create_page_font(self):
297 """Return frame of widgets for Font tab.
299 Fonts: Enable users to provisionally change font face, size, or
300 boldness and to see the consequence of proposed choices. Each
301 action set 3 options in changes structuree and changes the
302 corresponding aspect of the font sample on this page and
303 highlight sample on highlight page.
305 Function load_font_cfg initializes font vars and widgets from
306 idleConf entries and tk.
308 Fontlist: mouse button 1 click or up or down key invoke
309 on_fontlist_select(), which sets var font_name.
311 Sizelist: clicking the menubutton opens the dropdown menu. A
312 mouse button 1 click or return key sets var font_size.
314 Bold_toggle: clicking the box toggles var font_bold.
316 Changing any of the font vars invokes var_changed_font, which
317 adds all 3 font options to changes and calls set_samples.
318 Set_samples applies a new font constructed from the font vars to
319 font_sample and to highlight_sample on the highlight page.
321 Widgets for FontPage(Frame): (*) widgets bound to self
322 frame_font: LabelFrame
323 frame_font_name: Frame
324 font_name_title: Label
325 (*)fontlist: ListBox - font_name
326 scroll_font: Scrollbar
327 frame_font_param: Frame
328 font_size_title: Label
329 (*)sizelist: DynOptionMenu - font_size
330 (*)bold_toggle: Checkbutton - font_bold
331 frame_sample: LabelFrame
332 (*)font_sample: Label
333 """
334 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
335 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
336 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
338 # Define frames and widgets.
339 frame_font = LabelFrame(self, borderwidth=2, relief=GROOVE,
340 text=' Shell/Editor Font ')
341 frame_sample = LabelFrame(self, borderwidth=2, relief=GROOVE,
342 text=' Font Sample (Editable) ')
343 # frame_font.
344 frame_font_name = Frame(frame_font)
345 frame_font_param = Frame(frame_font)
346 font_name_title = Label(
347 frame_font_name, justify=LEFT, text='Font Face :')
348 self.fontlist = Listbox(frame_font_name, height=15,
349 takefocus=True, exportselection=FALSE)
350 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
351 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
352 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
353 scroll_font = Scrollbar(frame_font_name)
354 scroll_font.config(command=self.fontlist.yview)
355 self.fontlist.config(yscrollcommand=scroll_font.set)
356 font_size_title = Label(frame_font_param, text='Size :')
357 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
358 self.bold_toggle = Checkbutton(
359 frame_font_param, variable=self.font_bold,
360 onvalue=1, offvalue=0, text='Bold')
361 # frame_sample.
362 font_sample_frame = ScrollableTextFrame(frame_sample)
363 self.font_sample = font_sample_frame.text
364 self.font_sample.config(wrap=NONE, width=1, height=1)
365 self.font_sample.insert(END, font_sample_text)
367 # Grid and pack widgets:
368 self.columnconfigure(1, weight=1)
369 self.rowconfigure(2, weight=1)
370 frame_font.grid(row=0, column=0, padx=5, pady=5)
371 frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5,
372 sticky='nsew')
373 # frame_font.
374 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
375 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
376 font_name_title.pack(side=TOP, anchor=W)
377 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
378 scroll_font.pack(side=LEFT, fill=Y)
379 font_size_title.pack(side=LEFT, anchor=W)
380 self.sizelist.pack(side=LEFT, anchor=W)
381 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
382 # frame_sample.
383 font_sample_frame.pack(expand=TRUE, fill=BOTH)
385 def load_font_cfg(self):
386 """Load current configuration settings for the font options.
388 Retrieve current font with idleConf.GetFont and font families
389 from tk. Setup fontlist and set font_name. Setup sizelist,
390 which sets font_size. Set font_bold. Call set_samples.
391 """
392 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
393 font_name = configured_font[0].lower()
394 font_size = configured_font[1]
395 font_bold = configured_font[2]=='bold'
397 # Set sorted no-duplicate editor font selection list and font_name.
398 fonts = sorted(set(tkfont.families(self)))
399 for font in fonts:
400 self.fontlist.insert(END, font)
401 self.font_name.set(font_name)
402 lc_fonts = [s.lower() for s in fonts]
403 try:
404 current_font_index = lc_fonts.index(font_name)
405 self.fontlist.see(current_font_index)
406 self.fontlist.select_set(current_font_index)
407 self.fontlist.select_anchor(current_font_index)
408 self.fontlist.activate(current_font_index)
409 except ValueError:
410 pass
411 # Set font size dropdown.
412 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
413 '16', '18', '20', '22', '25', '29', '34', '40'),
414 font_size)
415 # Set font weight.
416 self.font_bold.set(font_bold)
417 self.set_samples()
419 def var_changed_font(self, *params):
420 """Store changes to font attributes.
422 When one font attribute changes, save them all, as they are
423 not independent from each other. In particular, when we are
424 overriding the default font, we need to write out everything.
425 """
426 value = self.font_name.get()
427 changes.add_option('main', 'EditorWindow', 'font', value)
428 value = self.font_size.get()
429 changes.add_option('main', 'EditorWindow', 'font-size', value)
430 value = self.font_bold.get()
431 changes.add_option('main', 'EditorWindow', 'font-bold', value)
432 self.set_samples()
434 def on_fontlist_select(self, event):
435 """Handle selecting a font from the list.
437 Event can result from either mouse click or Up or Down key.
438 Set font_name and example displays to selection.
439 """
440 font = self.fontlist.get(
441 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
442 self.font_name.set(font.lower())
444 def set_samples(self, event=None):
445 """Update update both screen samples with the font settings.
447 Called on font initialization and change events.
448 Accesses font_name, font_size, and font_bold Variables.
449 Updates font_sample and highlight page highlight_sample.
450 """
451 font_name = self.font_name.get()
452 font_weight = tkfont.BOLD if self.font_bold.get() else tkfont.NORMAL
453 new_font = (font_name, self.font_size.get(), font_weight)
454 self.font_sample['font'] = new_font
455 self.highlight_sample['font'] = new_font
458class HighPage(Frame):
460 def __init__(self, master, extpage):
461 super().__init__(master)
462 self.extpage = extpage
463 self.cd = master.winfo_toplevel()
464 self.style = Style(master)
465 self.create_page_highlight()
466 self.load_theme_cfg()
468 def create_page_highlight(self):
469 """Return frame of widgets for Highlights tab.
471 Enable users to provisionally change foreground and background
472 colors applied to textual tags. Color mappings are stored in
473 complete listings called themes. Built-in themes in
474 idlelib/config-highlight.def are fixed as far as the dialog is
475 concerned. Any theme can be used as the base for a new custom
476 theme, stored in .idlerc/config-highlight.cfg.
478 Function load_theme_cfg() initializes tk variables and theme
479 lists and calls paint_theme_sample() and set_highlight_target()
480 for the current theme. Radiobuttons builtin_theme_on and
481 custom_theme_on toggle var theme_source, which controls if the
482 current set of colors are from a builtin or custom theme.
483 DynOptionMenus builtinlist and customlist contain lists of the
484 builtin and custom themes, respectively, and the current item
485 from each list is stored in vars builtin_name and custom_name.
487 Function paint_theme_sample() applies the colors from the theme
488 to the tags in text widget highlight_sample and then invokes
489 set_color_sample(). Function set_highlight_target() sets the state
490 of the radiobuttons fg_on and bg_on based on the tag and it also
491 invokes set_color_sample().
493 Function set_color_sample() sets the background color for the frame
494 holding the color selector. This provides a larger visual of the
495 color for the current tag and plane (foreground/background).
497 Note: set_color_sample() is called from many places and is often
498 called more than once when a change is made. It is invoked when
499 foreground or background is selected (radiobuttons), from
500 paint_theme_sample() (theme is changed or load_cfg is called), and
501 from set_highlight_target() (target tag is changed or load_cfg called).
503 Button delete_custom invokes delete_custom() to delete
504 a custom theme from idleConf.userCfg['highlight'] and changes.
505 Button save_custom invokes save_as_new_theme() which calls
506 get_new_theme_name() and create_new() to save a custom theme
507 and its colors to idleConf.userCfg['highlight'].
509 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
510 if the current selected color for a tag is for the foreground or
511 background.
513 DynOptionMenu targetlist contains a readable description of the
514 tags applied to Python source within IDLE. Selecting one of the
515 tags from this list populates highlight_target, which has a callback
516 function set_highlight_target().
518 Text widget highlight_sample displays a block of text (which is
519 mock Python code) in which is embedded the defined tags and reflects
520 the color attributes of the current theme and changes for those tags.
521 Mouse button 1 allows for selection of a tag and updates
522 highlight_target with that tag value.
524 Note: The font in highlight_sample is set through the config in
525 the fonts tab.
527 In other words, a tag can be selected either from targetlist or
528 by clicking on the sample text within highlight_sample. The
529 plane (foreground/background) is selected via the radiobutton.
530 Together, these two (tag and plane) control what color is
531 shown in set_color_sample() for the current theme. Button set_color
532 invokes get_color() which displays a ColorChooser to change the
533 color for the selected tag/plane. If a new color is picked,
534 it will be saved to changes and the highlight_sample and
535 frame background will be updated.
537 Tk Variables:
538 color: Color of selected target.
539 builtin_name: Menu variable for built-in theme.
540 custom_name: Menu variable for custom theme.
541 fg_bg_toggle: Toggle for foreground/background color.
542 Note: this has no callback.
543 theme_source: Selector for built-in or custom theme.
544 highlight_target: Menu variable for the highlight tag target.
546 Instance Data Attributes:
547 theme_elements: Dictionary of tags for text highlighting.
548 The key is the display name and the value is a tuple of
549 (tag name, display sort order).
551 Methods [attachment]:
552 load_theme_cfg: Load current highlight colors.
553 get_color: Invoke colorchooser [button_set_color].
554 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
555 set_highlight_target: set fg_bg_toggle, set_color_sample().
556 set_color_sample: Set frame background to target.
557 on_new_color_set: Set new color and add option.
558 paint_theme_sample: Recolor sample.
559 get_new_theme_name: Get from popup.
560 create_new: Combine theme with changes and save.
561 save_as_new_theme: Save [button_save_custom].
562 set_theme_type: Command for [theme_source].
563 delete_custom: Activate default [button_delete_custom].
564 save_new: Save to userCfg['theme'] (is function).
566 Widgets of highlights page frame: (*) widgets bound to self
567 frame_custom: LabelFrame
568 (*)highlight_sample: Text
569 (*)frame_color_set: Frame
570 (*)button_set_color: Button
571 (*)targetlist: DynOptionMenu - highlight_target
572 frame_fg_bg_toggle: Frame
573 (*)fg_on: Radiobutton - fg_bg_toggle
574 (*)bg_on: Radiobutton - fg_bg_toggle
575 (*)button_save_custom: Button
576 frame_theme: LabelFrame
577 theme_type_title: Label
578 (*)builtin_theme_on: Radiobutton - theme_source
579 (*)custom_theme_on: Radiobutton - theme_source
580 (*)builtinlist: DynOptionMenu - builtin_name
581 (*)customlist: DynOptionMenu - custom_name
582 (*)button_delete_custom: Button
583 (*)theme_message: Label
584 """
585 self.theme_elements = {
586 'Normal Code or Text': ('normal', '00'),
587 'Code Context': ('context', '01'),
588 'Python Keywords': ('keyword', '02'),
589 'Python Definitions': ('definition', '03'),
590 'Python Builtins': ('builtin', '04'),
591 'Python Comments': ('comment', '05'),
592 'Python Strings': ('string', '06'),
593 'Selected Text': ('hilite', '07'),
594 'Found Text': ('hit', '08'),
595 'Cursor': ('cursor', '09'),
596 'Editor Breakpoint': ('break', '10'),
597 'Shell Prompt': ('console', '11'),
598 'Error Text': ('error', '12'),
599 'Shell User Output': ('stdout', '13'),
600 'Shell User Exception': ('stderr', '14'),
601 'Line Number': ('linenumber', '16'),
602 }
603 self.builtin_name = tracers.add(
604 StringVar(self), self.var_changed_builtin_name)
605 self.custom_name = tracers.add(
606 StringVar(self), self.var_changed_custom_name)
607 self.fg_bg_toggle = BooleanVar(self)
608 self.color = tracers.add(
609 StringVar(self), self.var_changed_color)
610 self.theme_source = tracers.add(
611 BooleanVar(self), self.var_changed_theme_source)
612 self.highlight_target = tracers.add(
613 StringVar(self), self.var_changed_highlight_target)
615 # Create widgets:
616 # body frame and section frames.
617 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
618 text=' Custom Highlighting ')
619 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
620 text=' Highlighting Theme ')
621 # frame_custom.
622 sample_frame = ScrollableTextFrame(
623 frame_custom, relief=SOLID, borderwidth=1)
624 text = self.highlight_sample = sample_frame.text
625 text.configure(
626 font=('courier', 12, ''), cursor='hand2', width=1, height=1,
627 takefocus=FALSE, highlightthickness=0, wrap=NONE)
628 # Prevent perhaps invisible selection of word or slice.
629 text.bind('<Double-Button-1>', lambda e: 'break')
630 text.bind('<B1-Motion>', lambda e: 'break')
631 string_tags=(
632 ('# Click selects item.', 'comment'), ('\n', 'normal'),
633 ('code context section', 'context'), ('\n', 'normal'),
634 ('| cursor', 'cursor'), ('\n', 'normal'),
635 ('def', 'keyword'), (' ', 'normal'),
636 ('func', 'definition'), ('(param):\n ', 'normal'),
637 ('"Return None."', 'string'), ('\n var0 = ', 'normal'),
638 ("'string'", 'string'), ('\n var1 = ', 'normal'),
639 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
640 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
641 ('list', 'builtin'), ('(', 'normal'),
642 ('None', 'keyword'), (')\n', 'normal'),
643 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
644 ('>>>', 'console'), (' 3.14**2\n', 'normal'),
645 ('9.8596', 'stdout'), ('\n', 'normal'),
646 ('>>>', 'console'), (' pri ', 'normal'),
647 ('n', 'error'), ('t(\n', 'normal'),
648 ('SyntaxError', 'stderr'), ('\n', 'normal'))
649 for string, tag in string_tags:
650 text.insert(END, string, tag)
651 n_lines = len(text.get('1.0', END).splitlines())
652 for lineno in range(1, n_lines):
653 text.insert(f'{lineno}.0',
654 f'{lineno:{len(str(n_lines))}d} ',
655 'linenumber')
656 for element in self.theme_elements:
657 def tem(event, elem=element):
658 # event.widget.winfo_top_level().highlight_target.set(elem)
659 self.highlight_target.set(elem)
660 text.tag_bind(
661 self.theme_elements[element][0], '<ButtonPress-1>', tem)
662 text['state'] = 'disabled'
663 self.style.configure('frame_color_set.TFrame', borderwidth=1,
664 relief='solid')
665 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
666 frame_fg_bg_toggle = Frame(frame_custom)
667 self.button_set_color = Button(
668 self.frame_color_set, text='Choose Color for :',
669 command=self.get_color)
670 self.targetlist = DynOptionMenu(
671 self.frame_color_set, self.highlight_target, None,
672 highlightthickness=0) #, command=self.set_highlight_targetBinding
673 self.fg_on = Radiobutton(
674 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
675 text='Foreground', command=self.set_color_sample_binding)
676 self.bg_on = Radiobutton(
677 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
678 text='Background', command=self.set_color_sample_binding)
679 self.fg_bg_toggle.set(1)
680 self.button_save_custom = Button(
681 frame_custom, text='Save as New Custom Theme',
682 command=self.save_as_new_theme)
683 # frame_theme.
684 theme_type_title = Label(frame_theme, text='Select : ')
685 self.builtin_theme_on = Radiobutton(
686 frame_theme, variable=self.theme_source, value=1,
687 command=self.set_theme_type, text='a Built-in Theme')
688 self.custom_theme_on = Radiobutton(
689 frame_theme, variable=self.theme_source, value=0,
690 command=self.set_theme_type, text='a Custom Theme')
691 self.builtinlist = DynOptionMenu(
692 frame_theme, self.builtin_name, None, command=None)
693 self.customlist = DynOptionMenu(
694 frame_theme, self.custom_name, None, command=None)
695 self.button_delete_custom = Button(
696 frame_theme, text='Delete Custom Theme',
697 command=self.delete_custom)
698 self.theme_message = Label(frame_theme, borderwidth=2)
699 # Pack widgets:
700 # body.
701 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
702 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
703 # frame_custom.
704 self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X)
705 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
706 sample_frame.pack(
707 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
708 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
709 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
710 self.fg_on.pack(side=LEFT, anchor=E)
711 self.bg_on.pack(side=RIGHT, anchor=W)
712 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
713 # frame_theme.
714 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
715 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
716 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
717 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
718 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
719 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
720 self.theme_message.pack(side=TOP, fill=X, pady=5)
722 def load_theme_cfg(self):
723 """Load current configuration settings for the theme options.
725 Based on the theme_source toggle, the theme is set as
726 either builtin or custom and the initial widget values
727 reflect the current settings from idleConf.
729 Attributes updated:
730 theme_source: Set from idleConf.
731 builtinlist: List of default themes from idleConf.
732 customlist: List of custom themes from idleConf.
733 custom_theme_on: Disabled if there are no custom themes.
734 custom_theme: Message with additional information.
735 targetlist: Create menu from self.theme_elements.
737 Methods:
738 set_theme_type
739 paint_theme_sample
740 set_highlight_target
741 """
742 # Set current theme type radiobutton.
743 self.theme_source.set(idleConf.GetOption(
744 'main', 'Theme', 'default', type='bool', default=1))
745 # Set current theme.
746 current_option = idleConf.CurrentTheme()
747 # Load available theme option menus.
748 if self.theme_source.get(): # Default theme selected.
749 item_list = idleConf.GetSectionList('default', 'highlight')
750 item_list.sort()
751 self.builtinlist.SetMenu(item_list, current_option)
752 item_list = idleConf.GetSectionList('user', 'highlight')
753 item_list.sort()
754 if not item_list:
755 self.custom_theme_on.state(('disabled',))
756 self.custom_name.set('- no custom themes -')
757 else:
758 self.customlist.SetMenu(item_list, item_list[0])
759 else: # User theme selected.
760 item_list = idleConf.GetSectionList('user', 'highlight')
761 item_list.sort()
762 self.customlist.SetMenu(item_list, current_option)
763 item_list = idleConf.GetSectionList('default', 'highlight')
764 item_list.sort()
765 self.builtinlist.SetMenu(item_list, item_list[0])
766 self.set_theme_type()
767 # Load theme element option menu.
768 theme_names = list(self.theme_elements.keys())
769 theme_names.sort(key=lambda x: self.theme_elements[x][1])
770 self.targetlist.SetMenu(theme_names, theme_names[0])
771 self.paint_theme_sample()
772 self.set_highlight_target()
774 def var_changed_builtin_name(self, *params):
775 """Process new builtin theme selection.
777 Add the changed theme's name to the changed_items and recreate
778 the sample with the values from the selected theme.
779 """
780 old_themes = ('IDLE Classic', 'IDLE New')
781 value = self.builtin_name.get()
782 if value not in old_themes:
783 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
784 changes.add_option('main', 'Theme', 'name', old_themes[0])
785 changes.add_option('main', 'Theme', 'name2', value)
786 self.theme_message['text'] = 'New theme, see Help'
787 else:
788 changes.add_option('main', 'Theme', 'name', value)
789 changes.add_option('main', 'Theme', 'name2', '')
790 self.theme_message['text'] = ''
791 self.paint_theme_sample()
793 def var_changed_custom_name(self, *params):
794 """Process new custom theme selection.
796 If a new custom theme is selected, add the name to the
797 changed_items and apply the theme to the sample.
798 """
799 value = self.custom_name.get()
800 if value != '- no custom themes -':
801 changes.add_option('main', 'Theme', 'name', value)
802 self.paint_theme_sample()
804 def var_changed_theme_source(self, *params):
805 """Process toggle between builtin and custom theme.
807 Update the default toggle value and apply the newly
808 selected theme type.
809 """
810 value = self.theme_source.get()
811 changes.add_option('main', 'Theme', 'default', value)
812 if value:
813 self.var_changed_builtin_name()
814 else:
815 self.var_changed_custom_name()
817 def var_changed_color(self, *params):
818 "Process change to color choice."
819 self.on_new_color_set()
821 def var_changed_highlight_target(self, *params):
822 "Process selection of new target tag for highlighting."
823 self.set_highlight_target()
825 def set_theme_type(self):
826 """Set available screen options based on builtin or custom theme.
828 Attributes accessed:
829 theme_source
831 Attributes updated:
832 builtinlist
833 customlist
834 button_delete_custom
835 custom_theme_on
837 Called from:
838 handler for builtin_theme_on and custom_theme_on
839 delete_custom
840 create_new
841 load_theme_cfg
842 """
843 if self.theme_source.get():
844 self.builtinlist['state'] = 'normal'
845 self.customlist['state'] = 'disabled'
846 self.button_delete_custom.state(('disabled',))
847 else:
848 self.builtinlist['state'] = 'disabled'
849 self.custom_theme_on.state(('!disabled',))
850 self.customlist['state'] = 'normal'
851 self.button_delete_custom.state(('!disabled',))
853 def get_color(self):
854 """Handle button to select a new color for the target tag.
856 If a new color is selected while using a builtin theme, a
857 name must be supplied to create a custom theme.
859 Attributes accessed:
860 highlight_target
861 frame_color_set
862 theme_source
864 Attributes updated:
865 color
867 Methods:
868 get_new_theme_name
869 create_new
870 """
871 target = self.highlight_target.get()
872 prev_color = self.style.lookup(self.frame_color_set['style'],
873 'background')
874 rgbTuplet, color_string = colorchooser.askcolor(
875 parent=self, title='Pick new color for : '+target,
876 initialcolor=prev_color)
877 if color_string and (color_string != prev_color):
878 # User didn't cancel and they chose a new color.
879 if self.theme_source.get(): # Current theme is a built-in.
880 message = ('Your changes will be saved as a new Custom Theme. '
881 'Enter a name for your new Custom Theme below.')
882 new_theme = self.get_new_theme_name(message)
883 if not new_theme: # User cancelled custom theme creation.
884 return
885 else: # Create new custom theme based on previously active theme.
886 self.create_new(new_theme)
887 self.color.set(color_string)
888 else: # Current theme is user defined.
889 self.color.set(color_string)
891 def on_new_color_set(self):
892 "Display sample of new color selection on the dialog."
893 new_color = self.color.get()
894 self.style.configure('frame_color_set.TFrame', background=new_color)
895 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
896 sample_element = self.theme_elements[self.highlight_target.get()][0]
897 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
898 theme = self.custom_name.get()
899 theme_element = sample_element + '-' + plane
900 changes.add_option('highlight', theme, theme_element, new_color)
902 def get_new_theme_name(self, message):
903 "Return name of new theme from query popup."
904 used_names = (idleConf.GetSectionList('user', 'highlight') +
905 idleConf.GetSectionList('default', 'highlight'))
906 new_theme = SectionName(
907 self, 'New Custom Theme', message, used_names).result
908 return new_theme
910 def save_as_new_theme(self):
911 """Prompt for new theme name and create the theme.
913 Methods:
914 get_new_theme_name
915 create_new
916 """
917 new_theme_name = self.get_new_theme_name('New Theme Name:')
918 if new_theme_name:
919 self.create_new(new_theme_name)
921 def create_new(self, new_theme_name):
922 """Create a new custom theme with the given name.
924 Create the new theme based on the previously active theme
925 with the current changes applied. Once it is saved, then
926 activate the new theme.
928 Attributes accessed:
929 builtin_name
930 custom_name
932 Attributes updated:
933 customlist
934 theme_source
936 Method:
937 save_new
938 set_theme_type
939 """
940 if self.theme_source.get():
941 theme_type = 'default'
942 theme_name = self.builtin_name.get()
943 else:
944 theme_type = 'user'
945 theme_name = self.custom_name.get()
946 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
947 # Apply any of the old theme's unsaved changes to the new theme.
948 if theme_name in changes['highlight']:
949 theme_changes = changes['highlight'][theme_name]
950 for element in theme_changes:
951 new_theme[element] = theme_changes[element]
952 # Save the new theme.
953 self.save_new(new_theme_name, new_theme)
954 # Change GUI over to the new theme.
955 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
956 custom_theme_list.sort()
957 self.customlist.SetMenu(custom_theme_list, new_theme_name)
958 self.theme_source.set(0)
959 self.set_theme_type()
961 def set_highlight_target(self):
962 """Set fg/bg toggle and color based on highlight tag target.
964 Instance variables accessed:
965 highlight_target
967 Attributes updated:
968 fg_on
969 bg_on
970 fg_bg_toggle
972 Methods:
973 set_color_sample
975 Called from:
976 var_changed_highlight_target
977 load_theme_cfg
978 """
979 if self.highlight_target.get() == 'Cursor': # bg not possible
980 self.fg_on.state(('disabled',))
981 self.bg_on.state(('disabled',))
982 self.fg_bg_toggle.set(1)
983 else: # Both fg and bg can be set.
984 self.fg_on.state(('!disabled',))
985 self.bg_on.state(('!disabled',))
986 self.fg_bg_toggle.set(1)
987 self.set_color_sample()
989 def set_color_sample_binding(self, *args):
990 """Change color sample based on foreground/background toggle.
992 Methods:
993 set_color_sample
994 """
995 self.set_color_sample()
997 def set_color_sample(self):
998 """Set the color of the frame background to reflect the selected target.
1000 Instance variables accessed:
1001 theme_elements
1002 highlight_target
1003 fg_bg_toggle
1004 highlight_sample
1006 Attributes updated:
1007 frame_color_set
1008 """
1009 # Set the color sample area.
1010 tag = self.theme_elements[self.highlight_target.get()][0]
1011 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1012 color = self.highlight_sample.tag_cget(tag, plane)
1013 self.style.configure('frame_color_set.TFrame', background=color)
1015 def paint_theme_sample(self):
1016 """Apply the theme colors to each element tag in the sample text.
1018 Instance attributes accessed:
1019 theme_elements
1020 theme_source
1021 builtin_name
1022 custom_name
1024 Attributes updated:
1025 highlight_sample: Set the tag elements to the theme.
1027 Methods:
1028 set_color_sample
1030 Called from:
1031 var_changed_builtin_name
1032 var_changed_custom_name
1033 load_theme_cfg
1034 """
1035 if self.theme_source.get(): # Default theme
1036 theme = self.builtin_name.get()
1037 else: # User theme
1038 theme = self.custom_name.get()
1039 for element_title in self.theme_elements:
1040 element = self.theme_elements[element_title][0]
1041 colors = idleConf.GetHighlight(theme, element)
1042 if element == 'cursor': # Cursor sample needs special painting.
1043 colors['background'] = idleConf.GetHighlight(
1044 theme, 'normal')['background']
1045 # Handle any unsaved changes to this theme.
1046 if theme in changes['highlight']:
1047 theme_dict = changes['highlight'][theme]
1048 if element + '-foreground' in theme_dict:
1049 colors['foreground'] = theme_dict[element + '-foreground']
1050 if element + '-background' in theme_dict:
1051 colors['background'] = theme_dict[element + '-background']
1052 self.highlight_sample.tag_config(element, **colors)
1053 self.set_color_sample()
1055 def save_new(self, theme_name, theme):
1056 """Save a newly created theme to idleConf.
1058 theme_name - string, the name of the new theme
1059 theme - dictionary containing the new theme
1060 """
1061 idleConf.userCfg['highlight'].AddSection(theme_name)
1062 for element in theme:
1063 value = theme[element]
1064 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1066 def askyesno(self, *args, **kwargs):
1067 # Make testing easier. Could change implementation.
1068 return messagebox.askyesno(*args, **kwargs)
1070 def delete_custom(self):
1071 """Handle event to delete custom theme.
1073 The current theme is deactivated and the default theme is
1074 activated. The custom theme is permanently removed from
1075 the config file.
1077 Attributes accessed:
1078 custom_name
1080 Attributes updated:
1081 custom_theme_on
1082 customlist
1083 theme_source
1084 builtin_name
1086 Methods:
1087 deactivate_current_config
1088 save_all_changed_extensions
1089 activate_config_changes
1090 set_theme_type
1091 """
1092 theme_name = self.custom_name.get()
1093 delmsg = 'Are you sure you wish to delete the theme %r ?'
1094 if not self.askyesno(
1095 'Delete Theme', delmsg % theme_name, parent=self):
1096 return
1097 self.cd.deactivate_current_config()
1098 # Remove theme from changes, config, and file.
1099 changes.delete_section('highlight', theme_name)
1100 # Reload user theme list.
1101 item_list = idleConf.GetSectionList('user', 'highlight')
1102 item_list.sort()
1103 if not item_list:
1104 self.custom_theme_on.state(('disabled',))
1105 self.customlist.SetMenu(item_list, '- no custom themes -')
1106 else:
1107 self.customlist.SetMenu(item_list, item_list[0])
1108 # Revert to default theme.
1109 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1110 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1111 # User can't back out of these changes, they must be applied now.
1112 changes.save_all()
1113 self.extpage.save_all_changed_extensions()
1114 self.cd.activate_config_changes()
1115 self.set_theme_type()
1118class KeysPage(Frame):
1120 def __init__(self, master, extpage):
1121 super().__init__(master)
1122 self.extpage = extpage
1123 self.cd = master.winfo_toplevel()
1124 self.create_page_keys()
1125 self.load_key_cfg()
1127 def create_page_keys(self):
1128 """Return frame of widgets for Keys tab.
1130 Enable users to provisionally change both individual and sets of
1131 keybindings (shortcut keys). Except for features implemented as
1132 extensions, keybindings are stored in complete sets called
1133 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1134 as far as the dialog is concerned. Any keyset can be used as the
1135 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1137 Function load_key_cfg() initializes tk variables and keyset
1138 lists and calls load_keys_list for the current keyset.
1139 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1140 keyset_source, which controls if the current set of keybindings
1141 are from a builtin or custom keyset. DynOptionMenus builtinlist
1142 and customlist contain lists of the builtin and custom keysets,
1143 respectively, and the current item from each list is stored in
1144 vars builtin_name and custom_name.
1146 Button delete_custom_keys invokes delete_custom_keys() to delete
1147 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1148 save_custom_keys invokes save_as_new_key_set() which calls
1149 get_new_keys_name() and create_new_key_set() to save a custom keyset
1150 and its keybindings to idleConf.userCfg['keys'].
1152 Listbox bindingslist contains all of the keybindings for the
1153 selected keyset. The keybindings are loaded in load_keys_list()
1154 and are pairs of (event, [keys]) where keys can be a list
1155 of one or more key combinations to bind to the same event.
1156 Mouse button 1 click invokes on_bindingslist_select(), which
1157 allows button_new_keys to be clicked.
1159 So, an item is selected in listbindings, which activates
1160 button_new_keys, and clicking button_new_keys calls function
1161 get_new_keys(). Function get_new_keys() gets the key mappings from the
1162 current keyset for the binding event item that was selected. The
1163 function then displays another dialog, GetKeysDialog, with the
1164 selected binding event and current keys and allows new key sequences
1165 to be entered for that binding event. If the keys aren't
1166 changed, nothing happens. If the keys are changed and the keyset
1167 is a builtin, function get_new_keys_name() will be called
1168 for input of a custom keyset name. If no name is given, then the
1169 change to the keybinding will abort and no updates will be made. If
1170 a custom name is entered in the prompt or if the current keyset was
1171 already custom (and thus didn't require a prompt), then
1172 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1173 with the change to the event binding. The item listing in bindingslist
1174 is updated with the new keys. Var keybinding is also set which invokes
1175 the callback function, var_changed_keybinding, to add the change to
1176 the 'keys' or 'extensions' changes tracker based on the binding type.
1178 Tk Variables:
1179 keybinding: Action/key bindings.
1181 Methods:
1182 load_keys_list: Reload active set.
1183 create_new_key_set: Combine active keyset and changes.
1184 set_keys_type: Command for keyset_source.
1185 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1186 deactivate_current_config: Remove keys bindings in editors.
1188 Widgets for KeysPage(frame): (*) widgets bound to self
1189 frame_key_sets: LabelFrame
1190 frames[0]: Frame
1191 (*)builtin_keyset_on: Radiobutton - var keyset_source
1192 (*)custom_keyset_on: Radiobutton - var keyset_source
1193 (*)builtinlist: DynOptionMenu - var builtin_name,
1194 func keybinding_selected
1195 (*)customlist: DynOptionMenu - var custom_name,
1196 func keybinding_selected
1197 (*)keys_message: Label
1198 frames[1]: Frame
1199 (*)button_delete_custom_keys: Button - delete_custom_keys
1200 (*)button_save_custom_keys: Button - save_as_new_key_set
1201 frame_custom: LabelFrame
1202 frame_target: Frame
1203 target_title: Label
1204 scroll_target_y: Scrollbar
1205 scroll_target_x: Scrollbar
1206 (*)bindingslist: ListBox - on_bindingslist_select
1207 (*)button_new_keys: Button - get_new_keys & ..._name
1208 """
1209 self.builtin_name = tracers.add(
1210 StringVar(self), self.var_changed_builtin_name)
1211 self.custom_name = tracers.add(
1212 StringVar(self), self.var_changed_custom_name)
1213 self.keyset_source = tracers.add(
1214 BooleanVar(self), self.var_changed_keyset_source)
1215 self.keybinding = tracers.add(
1216 StringVar(self), self.var_changed_keybinding)
1218 # Create widgets:
1219 # body and section frames.
1220 frame_custom = LabelFrame(
1221 self, borderwidth=2, relief=GROOVE,
1222 text=' Custom Key Bindings ')
1223 frame_key_sets = LabelFrame(
1224 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1225 # frame_custom.
1226 frame_target = Frame(frame_custom)
1227 target_title = Label(frame_target, text='Action - Key(s)')
1228 scroll_target_y = Scrollbar(frame_target)
1229 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1230 self.bindingslist = Listbox(
1231 frame_target, takefocus=FALSE, exportselection=FALSE)
1232 self.bindingslist.bind('<ButtonRelease-1>',
1233 self.on_bindingslist_select)
1234 scroll_target_y['command'] = self.bindingslist.yview
1235 scroll_target_x['command'] = self.bindingslist.xview
1236 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1237 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1238 self.button_new_keys = Button(
1239 frame_custom, text='Get New Keys for Selection',
1240 command=self.get_new_keys, state='disabled')
1241 # frame_key_sets.
1242 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
1243 for i in range(2)]
1244 self.builtin_keyset_on = Radiobutton(
1245 frames[0], variable=self.keyset_source, value=1,
1246 command=self.set_keys_type, text='Use a Built-in Key Set')
1247 self.custom_keyset_on = Radiobutton(
1248 frames[0], variable=self.keyset_source, value=0,
1249 command=self.set_keys_type, text='Use a Custom Key Set')
1250 self.builtinlist = DynOptionMenu(
1251 frames[0], self.builtin_name, None, command=None)
1252 self.customlist = DynOptionMenu(
1253 frames[0], self.custom_name, None, command=None)
1254 self.button_delete_custom_keys = Button(
1255 frames[1], text='Delete Custom Key Set',
1256 command=self.delete_custom_keys)
1257 self.button_save_custom_keys = Button(
1258 frames[1], text='Save as New Custom Key Set',
1259 command=self.save_as_new_key_set)
1260 self.keys_message = Label(frames[0], borderwidth=2)
1262 # Pack widgets:
1263 # body.
1264 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1265 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1266 # frame_custom.
1267 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1268 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1269 # frame_target.
1270 frame_target.columnconfigure(0, weight=1)
1271 frame_target.rowconfigure(1, weight=1)
1272 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1273 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1274 scroll_target_y.grid(row=1, column=1, sticky=NS)
1275 scroll_target_x.grid(row=2, column=0, sticky=EW)
1276 # frame_key_sets.
1277 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1278 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1279 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1280 self.customlist.grid(row=1, column=1, sticky=NSEW)
1281 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1282 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1283 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1284 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1285 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1287 def load_key_cfg(self):
1288 "Load current configuration settings for the keybinding options."
1289 # Set current keys type radiobutton.
1290 self.keyset_source.set(idleConf.GetOption(
1291 'main', 'Keys', 'default', type='bool', default=1))
1292 # Set current keys.
1293 current_option = idleConf.CurrentKeys()
1294 # Load available keyset option menus.
1295 if self.keyset_source.get(): # Default theme selected.
1296 item_list = idleConf.GetSectionList('default', 'keys')
1297 item_list.sort()
1298 self.builtinlist.SetMenu(item_list, current_option)
1299 item_list = idleConf.GetSectionList('user', 'keys')
1300 item_list.sort()
1301 if not item_list:
1302 self.custom_keyset_on.state(('disabled',))
1303 self.custom_name.set('- no custom keys -')
1304 else:
1305 self.customlist.SetMenu(item_list, item_list[0])
1306 else: # User key set selected.
1307 item_list = idleConf.GetSectionList('user', 'keys')
1308 item_list.sort()
1309 self.customlist.SetMenu(item_list, current_option)
1310 item_list = idleConf.GetSectionList('default', 'keys')
1311 item_list.sort()
1312 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1313 self.set_keys_type()
1314 # Load keyset element list.
1315 keyset_name = idleConf.CurrentKeys()
1316 self.load_keys_list(keyset_name)
1318 def var_changed_builtin_name(self, *params):
1319 "Process selection of builtin key set."
1320 old_keys = (
1321 'IDLE Classic Windows',
1322 'IDLE Classic Unix',
1323 'IDLE Classic Mac',
1324 'IDLE Classic OSX',
1325 )
1326 value = self.builtin_name.get()
1327 if value not in old_keys:
1328 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1329 changes.add_option('main', 'Keys', 'name', old_keys[0])
1330 changes.add_option('main', 'Keys', 'name2', value)
1331 self.keys_message['text'] = 'New key set, see Help'
1332 else:
1333 changes.add_option('main', 'Keys', 'name', value)
1334 changes.add_option('main', 'Keys', 'name2', '')
1335 self.keys_message['text'] = ''
1336 self.load_keys_list(value)
1338 def var_changed_custom_name(self, *params):
1339 "Process selection of custom key set."
1340 value = self.custom_name.get()
1341 if value != '- no custom keys -':
1342 changes.add_option('main', 'Keys', 'name', value)
1343 self.load_keys_list(value)
1345 def var_changed_keyset_source(self, *params):
1346 "Process toggle between builtin key set and custom key set."
1347 value = self.keyset_source.get()
1348 changes.add_option('main', 'Keys', 'default', value)
1349 if value:
1350 self.var_changed_builtin_name()
1351 else:
1352 self.var_changed_custom_name()
1354 def var_changed_keybinding(self, *params):
1355 "Store change to a keybinding."
1356 value = self.keybinding.get()
1357 key_set = self.custom_name.get()
1358 event = self.bindingslist.get(ANCHOR).split()[0]
1359 if idleConf.IsCoreBinding(event):
1360 changes.add_option('keys', key_set, event, value)
1361 else: # Event is an extension binding.
1362 ext_name = idleConf.GetExtnNameForEvent(event)
1363 ext_keybind_section = ext_name + '_cfgBindings'
1364 changes.add_option('extensions', ext_keybind_section, event, value)
1366 def set_keys_type(self):
1367 "Set available screen options based on builtin or custom key set."
1368 if self.keyset_source.get():
1369 self.builtinlist['state'] = 'normal'
1370 self.customlist['state'] = 'disabled'
1371 self.button_delete_custom_keys.state(('disabled',))
1372 else:
1373 self.builtinlist['state'] = 'disabled'
1374 self.custom_keyset_on.state(('!disabled',))
1375 self.customlist['state'] = 'normal'
1376 self.button_delete_custom_keys.state(('!disabled',))
1378 def get_new_keys(self):
1379 """Handle event to change key binding for selected line.
1381 A selection of a key/binding in the list of current
1382 bindings pops up a dialog to enter a new binding. If
1383 the current key set is builtin and a binding has
1384 changed, then a name for a custom key set needs to be
1385 entered for the change to be applied.
1386 """
1387 list_index = self.bindingslist.index(ANCHOR)
1388 binding = self.bindingslist.get(list_index)
1389 bind_name = binding.split()[0]
1390 if self.keyset_source.get():
1391 current_key_set_name = self.builtin_name.get()
1392 else:
1393 current_key_set_name = self.custom_name.get()
1394 current_bindings = idleConf.GetCurrentKeySet()
1395 if current_key_set_name in changes['keys']: # unsaved changes
1396 key_set_changes = changes['keys'][current_key_set_name]
1397 for event in key_set_changes:
1398 current_bindings[event] = key_set_changes[event].split()
1399 current_key_sequences = list(current_bindings.values())
1400 new_keys = GetKeysWindow(self, 'Get New Keys', bind_name,
1401 current_key_sequences).result
1402 if new_keys:
1403 if self.keyset_source.get(): # Current key set is a built-in.
1404 message = ('Your changes will be saved as a new Custom Key Set.'
1405 ' Enter a name for your new Custom Key Set below.')
1406 new_keyset = self.get_new_keys_name(message)
1407 if not new_keyset: # User cancelled custom key set creation.
1408 self.bindingslist.select_set(list_index)
1409 self.bindingslist.select_anchor(list_index)
1410 return
1411 else: # Create new custom key set based on previously active key set.
1412 self.create_new_key_set(new_keyset)
1413 self.bindingslist.delete(list_index)
1414 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1415 self.bindingslist.select_set(list_index)
1416 self.bindingslist.select_anchor(list_index)
1417 self.keybinding.set(new_keys)
1418 else:
1419 self.bindingslist.select_set(list_index)
1420 self.bindingslist.select_anchor(list_index)
1422 def get_new_keys_name(self, message):
1423 "Return new key set name from query popup."
1424 used_names = (idleConf.GetSectionList('user', 'keys') +
1425 idleConf.GetSectionList('default', 'keys'))
1426 new_keyset = SectionName(
1427 self, 'New Custom Key Set', message, used_names).result
1428 return new_keyset
1430 def save_as_new_key_set(self):
1431 "Prompt for name of new key set and save changes using that name."
1432 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1433 if new_keys_name:
1434 self.create_new_key_set(new_keys_name)
1436 def on_bindingslist_select(self, event):
1437 "Activate button to assign new keys to selected action."
1438 self.button_new_keys.state(('!disabled',))
1440 def create_new_key_set(self, new_key_set_name):
1441 """Create a new custom key set with the given name.
1443 Copy the bindings/keys from the previously active keyset
1444 to the new keyset and activate the new custom keyset.
1445 """
1446 if self.keyset_source.get():
1447 prev_key_set_name = self.builtin_name.get()
1448 else:
1449 prev_key_set_name = self.custom_name.get()
1450 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1451 new_keys = {}
1452 for event in prev_keys: # Add key set to changed items.
1453 event_name = event[2:-2] # Trim off the angle brackets.
1454 binding = ' '.join(prev_keys[event])
1455 new_keys[event_name] = binding
1456 # Handle any unsaved changes to prev key set.
1457 if prev_key_set_name in changes['keys']:
1458 key_set_changes = changes['keys'][prev_key_set_name]
1459 for event in key_set_changes:
1460 new_keys[event] = key_set_changes[event]
1461 # Save the new key set.
1462 self.save_new_key_set(new_key_set_name, new_keys)
1463 # Change GUI over to the new key set.
1464 custom_key_list = idleConf.GetSectionList('user', 'keys')
1465 custom_key_list.sort()
1466 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1467 self.keyset_source.set(0)
1468 self.set_keys_type()
1470 def load_keys_list(self, keyset_name):
1471 """Reload the list of action/key binding pairs for the active key set.
1473 An action/key binding can be selected to change the key binding.
1474 """
1475 reselect = False
1476 if self.bindingslist.curselection():
1477 reselect = True
1478 list_index = self.bindingslist.index(ANCHOR)
1479 keyset = idleConf.GetKeySet(keyset_name)
1480 bind_names = list(keyset.keys())
1481 bind_names.sort()
1482 self.bindingslist.delete(0, END)
1483 for bind_name in bind_names:
1484 key = ' '.join(keyset[bind_name])
1485 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1486 if keyset_name in changes['keys']:
1487 # Handle any unsaved changes to this key set.
1488 if bind_name in changes['keys'][keyset_name]:
1489 key = changes['keys'][keyset_name][bind_name]
1490 self.bindingslist.insert(END, bind_name+' - '+key)
1491 if reselect:
1492 self.bindingslist.see(list_index)
1493 self.bindingslist.select_set(list_index)
1494 self.bindingslist.select_anchor(list_index)
1496 @staticmethod
1497 def save_new_key_set(keyset_name, keyset):
1498 """Save a newly created core key set.
1500 Add keyset to idleConf.userCfg['keys'], not to disk.
1501 If the keyset doesn't exist, it is created. The
1502 binding/keys are taken from the keyset argument.
1504 keyset_name - string, the name of the new key set
1505 keyset - dictionary containing the new keybindings
1506 """
1507 idleConf.userCfg['keys'].AddSection(keyset_name)
1508 for event in keyset:
1509 value = keyset[event]
1510 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1512 def askyesno(self, *args, **kwargs):
1513 # Make testing easier. Could change implementation.
1514 return messagebox.askyesno(*args, **kwargs)
1516 def delete_custom_keys(self):
1517 """Handle event to delete a custom key set.
1519 Applying the delete deactivates the current configuration and
1520 reverts to the default. The custom key set is permanently
1521 deleted from the config file.
1522 """
1523 keyset_name = self.custom_name.get()
1524 delmsg = 'Are you sure you wish to delete the key set %r ?'
1525 if not self.askyesno(
1526 'Delete Key Set', delmsg % keyset_name, parent=self):
1527 return
1528 self.cd.deactivate_current_config()
1529 # Remove key set from changes, config, and file.
1530 changes.delete_section('keys', keyset_name)
1531 # Reload user key set list.
1532 item_list = idleConf.GetSectionList('user', 'keys')
1533 item_list.sort()
1534 if not item_list:
1535 self.custom_keyset_on.state(('disabled',))
1536 self.customlist.SetMenu(item_list, '- no custom keys -')
1537 else:
1538 self.customlist.SetMenu(item_list, item_list[0])
1539 # Revert to default key set.
1540 self.keyset_source.set(idleConf.defaultCfg['main']
1541 .Get('Keys', 'default'))
1542 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1543 or idleConf.default_keys())
1544 # User can't back out of these changes, they must be applied now.
1545 changes.save_all()
1546 self.extpage.save_all_changed_extensions()
1547 self.cd.activate_config_changes()
1548 self.set_keys_type()
1551class WinPage(Frame):
1553 def __init__(self, master):
1554 super().__init__(master)
1556 self.init_validators()
1557 self.create_page_windows()
1558 self.load_windows_cfg()
1560 def init_validators(self):
1561 digits_or_empty_re = re.compile(r'[0-9]*')
1562 def is_digits_or_empty(s):
1563 "Return 's is blank or contains only digits'"
1564 return digits_or_empty_re.fullmatch(s) is not None
1565 self.digits_only = (self.register(is_digits_or_empty), '%P',)
1567 def create_page_windows(self):
1568 """Return frame of widgets for Windows tab.
1570 Enable users to provisionally change general window options.
1571 Function load_windows_cfg initializes tk variable idleConf.
1572 Radiobuttons startup_shell_on and startup_editor_on set var
1573 startup_edit. Entry boxes win_width_int and win_height_int set var
1574 win_width and win_height. Setting var_name invokes the default
1575 callback that adds option to changes.
1577 Widgets for WinPage(Frame): > vars, bound to self
1578 frame_window: LabelFrame
1579 frame_run: Frame
1580 startup_title: Label
1581 startup_editor_on: Radiobutton > startup_edit
1582 startup_shell_on: Radiobutton > startup_edit
1583 frame_win_size: Frame
1584 win_size_title: Label
1585 win_width_title: Label
1586 win_width_int: Entry > win_width
1587 win_height_title: Label
1588 win_height_int: Entry > win_height
1589 frame_cursor: Frame
1590 indent_title: Label
1591 indent_chooser: Spinbox > indent_spaces
1592 blink_on: Checkbutton > cursor_blink
1593 frame_autocomplete: Frame
1594 auto_wait_title: Label
1595 auto_wait_int: Entry > autocomplete_wait
1596 frame_paren1: Frame
1597 paren_style_title: Label
1598 paren_style_type: OptionMenu > paren_style
1599 frame_paren2: Frame
1600 paren_time_title: Label
1601 paren_flash_time: Entry > flash_delay
1602 bell_on: Checkbutton > paren_bell
1603 frame_format: Frame
1604 format_width_title: Label
1605 format_width_int: Entry > format_width
1606 """
1607 # Integer values need StringVar because int('') raises.
1608 self.startup_edit = tracers.add(
1609 IntVar(self), ('main', 'General', 'editor-on-startup'))
1610 self.win_width = tracers.add(
1611 StringVar(self), ('main', 'EditorWindow', 'width'))
1612 self.win_height = tracers.add(
1613 StringVar(self), ('main', 'EditorWindow', 'height'))
1614 self.indent_spaces = tracers.add(
1615 StringVar(self), ('main', 'Indent', 'num-spaces'))
1616 self.cursor_blink = tracers.add(
1617 BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink'))
1618 self.autocomplete_wait = tracers.add(
1619 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1620 self.paren_style = tracers.add(
1621 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1622 self.flash_delay = tracers.add(
1623 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1624 self.paren_bell = tracers.add(
1625 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
1626 self.format_width = tracers.add(
1627 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
1629 # Create widgets:
1630 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1631 text=' Window Preferences')
1633 frame_run = Frame(frame_window, borderwidth=0)
1634 startup_title = Label(frame_run, text='At Startup')
1635 self.startup_editor_on = Radiobutton(
1636 frame_run, variable=self.startup_edit, value=1,
1637 text="Open Edit Window")
1638 self.startup_shell_on = Radiobutton(
1639 frame_run, variable=self.startup_edit, value=0,
1640 text='Open Shell Window')
1642 frame_win_size = Frame(frame_window, borderwidth=0)
1643 win_size_title = Label(
1644 frame_win_size, text='Initial Window Size (in characters)')
1645 win_width_title = Label(frame_win_size, text='Width')
1646 self.win_width_int = Entry(
1647 frame_win_size, textvariable=self.win_width, width=3,
1648 validatecommand=self.digits_only, validate='key',
1649 )
1650 win_height_title = Label(frame_win_size, text='Height')
1651 self.win_height_int = Entry(
1652 frame_win_size, textvariable=self.win_height, width=3,
1653 validatecommand=self.digits_only, validate='key',
1654 )
1656 frame_cursor = Frame(frame_window, borderwidth=0)
1657 indent_title = Label(frame_cursor,
1658 text='Indent spaces (4 is standard)')
1659 try:
1660 self.indent_chooser = Spinbox(
1661 frame_cursor, textvariable=self.indent_spaces,
1662 from_=1, to=10, width=2,
1663 validatecommand=self.digits_only, validate='key')
1664 except TclError:
1665 self.indent_chooser = Combobox(
1666 frame_cursor, textvariable=self.indent_spaces,
1667 state="readonly", values=list(range(1,11)), width=3)
1668 cursor_blink_title = Label(frame_cursor, text='Cursor Blink')
1669 self.cursor_blink_bool = Checkbutton(frame_cursor, text="Cursor blink",
1670 variable=self.cursor_blink)
1672 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1673 auto_wait_title = Label(frame_autocomplete,
1674 text='Completions Popup Wait (milliseconds)')
1675 self.auto_wait_int = Entry(
1676 frame_autocomplete, textvariable=self.autocomplete_wait,
1677 width=6, validatecommand=self.digits_only, validate='key')
1679 frame_paren1 = Frame(frame_window, borderwidth=0)
1680 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1681 self.paren_style_type = OptionMenu(
1682 frame_paren1, self.paren_style, 'expression',
1683 "opener","parens","expression")
1684 frame_paren2 = Frame(frame_window, borderwidth=0)
1685 paren_time_title = Label(
1686 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1687 '(0 is until next input)')
1688 self.paren_flash_time = Entry(
1689 frame_paren2, textvariable=self.flash_delay, width=6,
1690 validatecommand=self.digits_only, validate='key')
1691 self.bell_on = Checkbutton(
1692 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1693 frame_format = Frame(frame_window, borderwidth=0)
1694 format_width_title = Label(frame_format,
1695 text='Format Paragraph Max Width')
1696 self.format_width_int = Entry(
1697 frame_format, textvariable=self.format_width, width=4,
1698 validatecommand=self.digits_only, validate='key',
1699 )
1701 # Pack widgets:
1702 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1703 # frame_run.
1704 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
1705 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1706 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1707 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1708 # frame_win_size.
1709 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
1710 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1711 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1712 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1713 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1714 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
1715 # frame_cursor.
1716 frame_cursor.pack(side=TOP, padx=5, pady=0, fill=X)
1717 indent_title.pack(side=LEFT, anchor=W, padx=5)
1718 self.indent_chooser.pack(side=LEFT, anchor=W, padx=10)
1719 self.cursor_blink_bool.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1720 # frame_autocomplete.
1721 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
1722 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1723 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
1724 # frame_paren.
1725 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
1726 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1727 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
1728 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
1729 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
1730 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1731 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
1732 # frame_format.
1733 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
1734 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1735 self.format_width_int.pack(side=TOP, padx=10, pady=5)
1737 def load_windows_cfg(self):
1738 # Set variables for all windows.
1739 self.startup_edit.set(idleConf.GetOption(
1740 'main', 'General', 'editor-on-startup', type='bool'))
1741 self.win_width.set(idleConf.GetOption(
1742 'main', 'EditorWindow', 'width', type='int'))
1743 self.win_height.set(idleConf.GetOption(
1744 'main', 'EditorWindow', 'height', type='int'))
1745 self.indent_spaces.set(idleConf.GetOption(
1746 'main', 'Indent', 'num-spaces', type='int'))
1747 self.cursor_blink.set(idleConf.GetOption(
1748 'main', 'EditorWindow', 'cursor-blink', type='bool'))
1749 self.autocomplete_wait.set(idleConf.GetOption(
1750 'extensions', 'AutoComplete', 'popupwait', type='int'))
1751 self.paren_style.set(idleConf.GetOption(
1752 'extensions', 'ParenMatch', 'style'))
1753 self.flash_delay.set(idleConf.GetOption(
1754 'extensions', 'ParenMatch', 'flash-delay', type='int'))
1755 self.paren_bell.set(idleConf.GetOption(
1756 'extensions', 'ParenMatch', 'bell'))
1757 self.format_width.set(idleConf.GetOption(
1758 'extensions', 'FormatParagraph', 'max-width', type='int'))
1761class ShedPage(Frame):
1763 def __init__(self, master):
1764 super().__init__(master)
1766 self.init_validators()
1767 self.create_page_shed()
1768 self.load_shelled_cfg()
1770 def init_validators(self):
1771 digits_or_empty_re = re.compile(r'[0-9]*')
1772 def is_digits_or_empty(s):
1773 "Return 's is blank or contains only digits'"
1774 return digits_or_empty_re.fullmatch(s) is not None
1775 self.digits_only = (self.register(is_digits_or_empty), '%P',)
1777 def create_page_shed(self):
1778 """Return frame of widgets for Shell/Ed tab.
1780 Enable users to provisionally change shell and editor options.
1781 Function load_shed_cfg initializes tk variables using idleConf.
1782 Entry box auto_squeeze_min_lines_int sets
1783 auto_squeeze_min_lines_int. Setting var_name invokes the
1784 default callback that adds option to changes.
1786 Widgets for ShedPage(Frame): (*) widgets bound to self
1787 frame_shell: LabelFrame
1788 frame_auto_squeeze_min_lines: Frame
1789 auto_squeeze_min_lines_title: Label
1790 (*)auto_squeeze_min_lines_int: Entry -
1791 auto_squeeze_min_lines
1792 frame_editor: LabelFrame
1793 frame_save: Frame
1794 run_save_title: Label
1795 (*)save_ask_on: Radiobutton - autosave
1796 (*)save_auto_on: Radiobutton - autosave
1797 frame_format: Frame
1798 format_width_title: Label
1799 (*)format_width_int: Entry - format_width
1800 frame_line_numbers_default: Frame
1801 line_numbers_default_title: Label
1802 (*)line_numbers_default_bool: Checkbutton - line_numbers_default
1803 frame_context: Frame
1804 context_title: Label
1805 (*)context_int: Entry - context_lines
1806 """
1807 # Integer values need StringVar because int('') raises.
1808 self.auto_squeeze_min_lines = tracers.add(
1809 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
1811 self.autosave = tracers.add(
1812 IntVar(self), ('main', 'General', 'autosave'))
1813 self.line_numbers_default = tracers.add(
1814 BooleanVar(self),
1815 ('main', 'EditorWindow', 'line-numbers-default'))
1816 self.context_lines = tracers.add(
1817 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
1819 # Create widgets:
1820 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
1821 text=' Shell Preferences')
1822 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1823 text=' Editor Preferences')
1824 # Frame_shell.
1825 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
1826 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
1827 text='Auto-Squeeze Min. Lines:')
1828 self.auto_squeeze_min_lines_int = Entry(
1829 frame_auto_squeeze_min_lines, width=4,
1830 textvariable=self.auto_squeeze_min_lines,
1831 validatecommand=self.digits_only, validate='key',
1832 )
1833 # Frame_editor.
1834 frame_save = Frame(frame_editor, borderwidth=0)
1835 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1837 self.save_ask_on = Radiobutton(
1838 frame_save, variable=self.autosave, value=0,
1839 text="Prompt to Save")
1840 self.save_auto_on = Radiobutton(
1841 frame_save, variable=self.autosave, value=1,
1842 text='No Prompt')
1844 frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
1845 line_numbers_default_title = Label(
1846 frame_line_numbers_default, text='Show line numbers in new windows')
1847 self.line_numbers_default_bool = Checkbutton(
1848 frame_line_numbers_default,
1849 variable=self.line_numbers_default,
1850 width=1)
1852 frame_context = Frame(frame_editor, borderwidth=0)
1853 context_title = Label(frame_context, text='Max Context Lines :')
1854 self.context_int = Entry(
1855 frame_context, textvariable=self.context_lines, width=3,
1856 validatecommand=self.digits_only, validate='key',
1857 )
1859 # Pack widgets:
1860 frame_shell.pack(side=TOP, padx=5, pady=5, fill=BOTH)
1861 Label(self).pack() # Spacer -- better solution?
1862 frame_editor.pack(side=TOP, padx=5, pady=5, fill=BOTH)
1863 # frame_auto_squeeze_min_lines
1864 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
1865 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1866 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
1867 # frame_save.
1868 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
1869 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1870 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1871 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1872 # frame_line_numbers_default.
1873 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
1874 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1875 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
1876 # frame_context.
1877 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
1878 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1879 self.context_int.pack(side=TOP, padx=5, pady=5)
1881 def load_shelled_cfg(self):
1882 # Set variables for shell windows.
1883 self.auto_squeeze_min_lines.set(idleConf.GetOption(
1884 'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
1885 # Set variables for editor windows.
1886 self.autosave.set(idleConf.GetOption(
1887 'main', 'General', 'autosave', default=0, type='bool'))
1888 self.line_numbers_default.set(idleConf.GetOption(
1889 'main', 'EditorWindow', 'line-numbers-default', type='bool'))
1890 self.context_lines.set(idleConf.GetOption(
1891 'extensions', 'CodeContext', 'maxlines', type='int'))
1894class ExtPage(Frame):
1895 def __init__(self, master):
1896 super().__init__(master)
1897 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
1898 self.ext_userCfg = idleConf.userCfg['extensions']
1899 self.is_int = self.register(is_int)
1900 self.load_extensions()
1901 self.create_page_extensions() # Requires extension names.
1903 def create_page_extensions(self):
1904 """Configure IDLE feature extensions and help menu extensions.
1906 List the feature extensions and a configuration box for the
1907 selected extension. Help menu extensions are in a HelpFrame.
1909 This code reads the current configuration using idleConf,
1910 supplies a GUI interface to change the configuration values,
1911 and saves the changes using idleConf.
1913 Some changes may require restarting IDLE. This depends on each
1914 extension's implementation.
1916 All values are treated as text, and it is up to the user to
1917 supply reasonable values. The only exception to this are the
1918 'enable*' options, which are boolean, and can be toggled with a
1919 True/False button.
1921 Methods:
1922 extension_selected: Handle selection from list.
1923 create_extension_frame: Hold widgets for one extension.
1924 set_extension_value: Set in userCfg['extensions'].
1925 save_all_changed_extensions: Call extension page Save().
1926 """
1927 self.extension_names = StringVar(self)
1929 frame_ext = LabelFrame(self, borderwidth=2, relief=GROOVE,
1930 text=' Feature Extensions ')
1931 self.frame_help = HelpFrame(self, borderwidth=2, relief=GROOVE,
1932 text=' Help Menu Extensions ')
1934 frame_ext.rowconfigure(0, weight=1)
1935 frame_ext.columnconfigure(2, weight=1)
1936 self.extension_list = Listbox(frame_ext, listvariable=self.extension_names,
1937 selectmode='browse')
1938 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
1939 scroll = Scrollbar(frame_ext, command=self.extension_list.yview)
1940 self.extension_list.yscrollcommand=scroll.set
1941 self.details_frame = LabelFrame(frame_ext, width=250, height=250)
1942 self.extension_list.grid(column=0, row=0, sticky='nws')
1943 scroll.grid(column=1, row=0, sticky='ns')
1944 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
1945 frame_ext.configure(padding=10)
1946 self.config_frame = {}
1947 self.current_extension = None
1949 self.outerframe = self # TEMPORARY
1950 self.tabbed_page_set = self.extension_list # TEMPORARY
1952 # Create the frame holding controls for each extension.
1953 ext_names = ''
1954 for ext_name in sorted(self.extensions):
1955 self.create_extension_frame(ext_name)
1956 ext_names = ext_names + '{' + ext_name + '} '
1957 self.extension_names.set(ext_names)
1958 self.extension_list.selection_set(0)
1959 self.extension_selected(None)
1962 frame_ext.grid(row=0, column=0, sticky='nsew')
1963 Label(self).grid(row=1, column=0) # Spacer. Replace with config?
1964 self.frame_help.grid(row=2, column=0, sticky='sew')
1966 def load_extensions(self):
1967 "Fill self.extensions with data from the default and user configs."
1968 self.extensions = {}
1969 for ext_name in idleConf.GetExtensions(active_only=False):
1970 # Former built-in extensions are already filtered out.
1971 self.extensions[ext_name] = []
1973 for ext_name in self.extensions:
1974 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
1976 # Bring 'enable' options to the beginning of the list.
1977 enables = [opt_name for opt_name in opt_list
1978 if opt_name.startswith('enable')]
1979 for opt_name in enables:
1980 opt_list.remove(opt_name)
1981 opt_list = enables + opt_list
1983 for opt_name in opt_list:
1984 def_str = self.ext_defaultCfg.Get(
1985 ext_name, opt_name, raw=True)
1986 try:
1987 def_obj = {'True':True, 'False':False}[def_str]
1988 opt_type = 'bool'
1989 except KeyError:
1990 try:
1991 def_obj = int(def_str)
1992 opt_type = 'int'
1993 except ValueError:
1994 def_obj = def_str
1995 opt_type = None
1996 try:
1997 value = self.ext_userCfg.Get(
1998 ext_name, opt_name, type=opt_type, raw=True,
1999 default=def_obj)
2000 except ValueError: # Need this until .Get fixed.
2001 value = def_obj # Bad values overwritten by entry.
2002 var = StringVar(self)
2003 var.set(str(value))
2005 self.extensions[ext_name].append({'name': opt_name,
2006 'type': opt_type,
2007 'default': def_str,
2008 'value': value,
2009 'var': var,
2010 })
2012 def extension_selected(self, event):
2013 "Handle selection of an extension from the list."
2014 newsel = self.extension_list.curselection()
2015 if newsel:
2016 newsel = self.extension_list.get(newsel)
2017 if newsel is None or newsel != self.current_extension:
2018 if self.current_extension:
2019 self.details_frame.config(text='')
2020 self.config_frame[self.current_extension].grid_forget()
2021 self.current_extension = None
2022 if newsel:
2023 self.details_frame.config(text=newsel)
2024 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
2025 self.current_extension = newsel
2027 def create_extension_frame(self, ext_name):
2028 """Create a frame holding the widgets to configure one extension"""
2029 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
2030 self.config_frame[ext_name] = f
2031 entry_area = f.interior
2032 # Create an entry for each configuration option.
2033 for row, opt in enumerate(self.extensions[ext_name]):
2034 # Create a row with a label and entry/checkbutton.
2035 label = Label(entry_area, text=opt['name'])
2036 label.grid(row=row, column=0, sticky=NW)
2037 var = opt['var']
2038 if opt['type'] == 'bool':
2039 Checkbutton(entry_area, variable=var,
2040 onvalue='True', offvalue='False', width=8
2041 ).grid(row=row, column=1, sticky=W, padx=7)
2042 elif opt['type'] == 'int':
2043 Entry(entry_area, textvariable=var, validate='key',
2044 validatecommand=(self.is_int, '%P'), width=10
2045 ).grid(row=row, column=1, sticky=NSEW, padx=7)
2047 else: # type == 'str'
2048 # Limit size to fit non-expanding space with larger font.
2049 Entry(entry_area, textvariable=var, width=15
2050 ).grid(row=row, column=1, sticky=NSEW, padx=7)
2051 return
2053 def set_extension_value(self, section, opt):
2054 """Return True if the configuration was added or changed.
2056 If the value is the same as the default, then remove it
2057 from user config file.
2058 """
2059 name = opt['name']
2060 default = opt['default']
2061 value = opt['var'].get().strip() or default
2062 opt['var'].set(value)
2063 # if self.defaultCfg.has_section(section):
2064 # Currently, always true; if not, indent to return.
2065 if (value == default):
2066 return self.ext_userCfg.RemoveOption(section, name)
2067 # Set the option.
2068 return self.ext_userCfg.SetOption(section, name, value)
2070 def save_all_changed_extensions(self):
2071 """Save configuration changes to the user config file.
2073 Attributes accessed:
2074 extensions
2076 Methods:
2077 set_extension_value
2078 """
2079 has_changes = False
2080 for ext_name in self.extensions:
2081 options = self.extensions[ext_name]
2082 for opt in options:
2083 if self.set_extension_value(ext_name, opt):
2084 has_changes = True
2085 if has_changes:
2086 self.ext_userCfg.Save()
2089class HelpFrame(LabelFrame):
2091 def __init__(self, master, **cfg):
2092 super().__init__(master, **cfg)
2093 self.create_frame_help()
2094 self.load_helplist()
2096 def create_frame_help(self):
2097 """Create LabelFrame for additional help menu sources.
2099 load_helplist loads list user_helplist with
2100 name, position pairs and copies names to listbox helplist.
2101 Clicking a name invokes help_source selected. Clicking
2102 button_helplist_name invokes helplist_item_name, which also
2103 changes user_helplist. These functions all call
2104 set_add_delete_state. All but load call update_help_changes to
2105 rewrite changes['main']['HelpFiles'].
2107 Widgets for HelpFrame(LabelFrame): (*) widgets bound to self
2108 frame_helplist: Frame
2109 (*)helplist: ListBox
2110 scroll_helplist: Scrollbar
2111 frame_buttons: Frame
2112 (*)button_helplist_edit
2113 (*)button_helplist_add
2114 (*)button_helplist_remove
2115 """
2116 # self = frame_help in dialog (until ExtPage class).
2117 frame_helplist = Frame(self)
2118 self.helplist = Listbox(
2119 frame_helplist, height=5, takefocus=True,
2120 exportselection=FALSE)
2121 scroll_helplist = Scrollbar(frame_helplist)
2122 scroll_helplist['command'] = self.helplist.yview
2123 self.helplist['yscrollcommand'] = scroll_helplist.set
2124 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
2126 frame_buttons = Frame(self)
2127 self.button_helplist_edit = Button(
2128 frame_buttons, text='Edit', state='disabled',
2129 width=8, command=self.helplist_item_edit)
2130 self.button_helplist_add = Button(
2131 frame_buttons, text='Add',
2132 width=8, command=self.helplist_item_add)
2133 self.button_helplist_remove = Button(
2134 frame_buttons, text='Remove', state='disabled',
2135 width=8, command=self.helplist_item_remove)
2137 # Pack frame_help.
2138 frame_helplist.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
2139 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
2140 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
2141 frame_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
2142 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
2143 self.button_helplist_add.pack(side=TOP, anchor=W)
2144 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
2146 def help_source_selected(self, event):
2147 "Handle event for selecting additional help."
2148 self.set_add_delete_state()
2150 def set_add_delete_state(self):
2151 "Toggle the state for the help list buttons based on list entries."
2152 if self.helplist.size() < 1: # No entries in list.
2153 self.button_helplist_edit.state(('disabled',))
2154 self.button_helplist_remove.state(('disabled',))
2155 else: # Some entries.
2156 if self.helplist.curselection(): # There currently is a selection.
2157 self.button_helplist_edit.state(('!disabled',))
2158 self.button_helplist_remove.state(('!disabled',))
2159 else: # There currently is not a selection.
2160 self.button_helplist_edit.state(('disabled',))
2161 self.button_helplist_remove.state(('disabled',))
2163 def helplist_item_add(self):
2164 """Handle add button for the help list.
2166 Query for name and location of new help sources and add
2167 them to the list.
2168 """
2169 help_source = HelpSource(self, 'New Help Source').result
2170 if help_source:
2171 self.user_helplist.append(help_source)
2172 self.helplist.insert(END, help_source[0])
2173 self.update_help_changes()
2175 def helplist_item_edit(self):
2176 """Handle edit button for the help list.
2178 Query with existing help source information and update
2179 config if the values are changed.
2180 """
2181 item_index = self.helplist.index(ANCHOR)
2182 help_source = self.user_helplist[item_index]
2183 new_help_source = HelpSource(
2184 self, 'Edit Help Source',
2185 menuitem=help_source[0],
2186 filepath=help_source[1],
2187 ).result
2188 if new_help_source and new_help_source != help_source:
2189 self.user_helplist[item_index] = new_help_source
2190 self.helplist.delete(item_index)
2191 self.helplist.insert(item_index, new_help_source[0])
2192 self.update_help_changes()
2193 self.set_add_delete_state() # Selected will be un-selected
2195 def helplist_item_remove(self):
2196 """Handle remove button for the help list.
2198 Delete the help list item from config.
2199 """
2200 item_index = self.helplist.index(ANCHOR)
2201 del(self.user_helplist[item_index])
2202 self.helplist.delete(item_index)
2203 self.update_help_changes()
2204 self.set_add_delete_state()
2206 def update_help_changes(self):
2207 "Clear and rebuild the HelpFiles section in changes"
2208 changes['main']['HelpFiles'] = {}
2209 for num in range(1, len(self.user_helplist) + 1):
2210 changes.add_option(
2211 'main', 'HelpFiles', str(num),
2212 ';'.join(self.user_helplist[num-1][:2]))
2214 def load_helplist(self):
2215 # Set additional help sources.
2216 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2217 self.helplist.delete(0, 'end')
2218 for help_item in self.user_helplist:
2219 self.helplist.insert(END, help_item[0])
2220 self.set_add_delete_state()
2223class VarTrace:
2224 """Maintain Tk variables trace state."""
2226 def __init__(self):
2227 """Store Tk variables and callbacks.
2229 untraced: List of tuples (var, callback)
2230 that do not have the callback attached
2231 to the Tk var.
2232 traced: List of tuples (var, callback) where
2233 that callback has been attached to the var.
2234 """
2235 self.untraced = []
2236 self.traced = []
2238 def clear(self):
2239 "Clear lists (for tests)."
2240 # Call after all tests in a module to avoid memory leaks.
2241 self.untraced.clear()
2242 self.traced.clear()
2244 def add(self, var, callback):
2245 """Add (var, callback) tuple to untraced list.
2247 Args:
2248 var: Tk variable instance.
2249 callback: Either function name to be used as a callback
2250 or a tuple with IdleConf config-type, section, and
2251 option names used in the default callback.
2253 Return:
2254 Tk variable instance.
2255 """
2256 if isinstance(callback, tuple):
2257 callback = self.make_callback(var, callback)
2258 self.untraced.append((var, callback))
2259 return var
2261 @staticmethod
2262 def make_callback(var, config):
2263 "Return default callback function to add values to changes instance."
2264 def default_callback(*params):
2265 "Add config values to changes instance."
2266 changes.add_option(*config, var.get())
2267 return default_callback
2269 def attach(self):
2270 "Attach callback to all vars that are not traced."
2271 while self.untraced:
2272 var, callback = self.untraced.pop()
2273 var.trace_add('write', callback)
2274 self.traced.append((var, callback))
2276 def detach(self):
2277 "Remove callback from traced vars."
2278 while self.traced:
2279 var, callback = self.traced.pop()
2280 var.trace_remove('write', var.trace_info()[0][1])
2281 self.untraced.append((var, callback))
2284tracers = VarTrace()
2286help_common = '''\
2287When you click either the Apply or Ok buttons, settings in this
2288dialog that are different from IDLE's default are saved in
2289a .idlerc directory in your home directory. Except as noted,
2290these changes apply to all versions of IDLE installed on this
2291machine. [Cancel] only cancels changes made since the last save.
2292'''
2293help_pages = {
2294 'Fonts/Tabs':'''
2295Font sample: This shows what a selection of Basic Multilingual Plane
2296unicode characters look like for the current font selection. If the
2297selected font does not define a character, Tk attempts to find another
2298font that does. Substitute glyphs depend on what is available on a
2299particular system and will not necessarily have the same size as the
2300font selected. Line contains 20 characters up to Devanagari, 14 for
2301Tamil, and 10 for East Asia.
2303Hebrew and Arabic letters should display right to left, starting with
2304alef, \u05d0 and \u0627. Arabic digits display left to right. The
2305Devanagari and Tamil lines start with digits. The East Asian lines
2306are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2307Hiragana and Katakana.
2309You can edit the font sample. Changes remain until IDLE is closed.
2310''',
2311 'Highlights': '''
2312Highlighting:
2313The IDLE Dark color theme is new in October 2015. It can only
2314be used with older IDLE releases if it is saved as a custom
2315theme, with a different name.
2316''',
2317 'Keys': '''
2318Keys:
2319The IDLE Modern Unix key set is new in June 2016. It can only
2320be used with older IDLE releases if it is saved as a custom
2321key set, with a different name.
2322''',
2323 'General': '''
2324General:
2326AutoComplete: Popupwait is milliseconds to wait after key char, without
2327cursor movement, before popping up completion box. Key char is '.' after
2328identifier or a '/' (or '\\' on Windows) within a string.
2330FormatParagraph: Max-width is max chars in lines after re-formatting.
2331Use with paragraphs in both strings and comment blocks.
2333ParenMatch: Style indicates what is highlighted when closer is entered:
2334'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2335'expression' (default) - also everything in between. Flash-delay is how
2336long to highlight if cursor is not moved (0 means forever).
2338CodeContext: Maxlines is the maximum number of code context lines to
2339display when Code Context is turned on for an editor window.
2341Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
2342of output to automatically "squeeze".
2343''',
2344 'Extensions': '''
2345ZzDummy: This extension is provided as an example for how to create and
2346use an extension. Enable indicates whether the extension is active or
2347not; likewise enable_editor and enable_shell indicate which windows it
2348will be active on. For this extension, z-text is the text that will be
2349inserted at or removed from the beginning of the lines of selected text,
2350or the current line if no selection.
2351''',
2352}
2355def is_int(s):
2356 "Return 's is blank or represents an int'"
2357 if not s:
2358 return True
2359 try:
2360 int(s)
2361 return True
2362 except ValueError:
2363 return False
2366class VerticalScrolledFrame(Frame):
2367 """A pure Tkinter vertically scrollable frame.
2369 * Use the 'interior' attribute to place widgets inside the scrollable frame
2370 * Construct and pack/place/grid normally
2371 * This frame only allows vertical scrolling
2372 """
2373 def __init__(self, parent, *args, **kw):
2374 Frame.__init__(self, parent, *args, **kw)
2376 # Create a canvas object and a vertical scrollbar for scrolling it.
2377 vscrollbar = Scrollbar(self, orient=VERTICAL)
2378 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
2379 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
2380 yscrollcommand=vscrollbar.set, width=240)
2381 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2382 vscrollbar.config(command=canvas.yview)
2384 # Reset the view.
2385 canvas.xview_moveto(0)
2386 canvas.yview_moveto(0)
2388 # Create a frame inside the canvas which will be scrolled with it.
2389 self.interior = interior = Frame(canvas)
2390 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2392 # Track changes to the canvas and frame width and sync them,
2393 # also updating the scrollbar.
2394 def _configure_interior(event):
2395 # Update the scrollbars to match the size of the inner frame.
2396 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2397 canvas.config(scrollregion="0 0 %s %s" % size)
2398 interior.bind('<Configure>', _configure_interior)
2400 def _configure_canvas(event):
2401 if interior.winfo_reqwidth() != canvas.winfo_width():
2402 # Update the inner frame's width to fill the canvas.
2403 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2404 canvas.bind('<Configure>', _configure_canvas)
2406 return
2409if __name__ == '__main__': 2409 ↛ 2410line 2409 didn't jump to line 2410, because the condition on line 2409 was never true
2410 from unittest import main
2411 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
2413 from idlelib.idle_test.htest import run
2414 run(ConfigDialog)