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