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