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