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