blob: 6638c062d254937275326ee23ca9a3438485d3f6 [file] [log] [blame]
Kurt B. Kaisere7a161e2003-01-10 20:13:57 +00001"""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.
Kurt B. Kaiseracdef852005-01-31 03:34:26 +00009Refer to comments in EditorWindow autoindent code for details.
Kurt B. Kaisere7a161e2003-01-10 20:13:57 +000010
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000011"""
Tal Einat1ebee372019-07-23 13:02:11 +030012import re
13
Victor Stinner6900f162020-04-30 03:28:51 +020014from tkinter import (Toplevel, Listbox, Scale, Canvas,
csabellabac7d332017-06-26 17:46:26 -040015 StringVar, BooleanVar, IntVar, TRUE, FALSE,
Terry Jan Reedye8f7c782017-11-28 21:52:32 -050016 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE,
17 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
Louie Lubb2bae82017-07-10 06:57:18 +080018 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -050019from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label,
wohlganger58fc71c2017-09-10 16:19:47 -050020 OptionMenu, Notebook, Radiobutton, Scrollbar, Style)
Terry Jan Reedy879986d2021-01-25 06:33:18 -050021from tkinter import colorchooser
22import tkinter.font as tkfont
Terry Jan Reedy3457f422017-08-27 16:39:41 -040023from tkinter import messagebox
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000024
terryjreedy349abd92017-07-07 16:00:57 -040025from idlelib.config import idleConf, ConfigChanges
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040026from idlelib.config_key import GetKeysDialog
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040027from idlelib.dynoption import DynOptionMenu
28from idlelib import macosx
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040029from idlelib.query import SectionName, HelpSource
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040030from idlelib.textview import view_text
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040031from idlelib.autocomplete import AutoComplete
32from idlelib.codecontext import CodeContext
33from idlelib.parenmatch import ParenMatch
Cheryl Sabella82494aa2019-07-17 09:44:44 -040034from idlelib.format import FormatParagraph
Tal Einat604e7b92018-09-25 15:10:14 +030035from idlelib.squeezer import Squeezer
Tal Einat3221a632019-07-27 19:57:48 +030036from idlelib.textview import ScrollableTextFrame
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -040037
terryjreedy349abd92017-07-07 16:00:57 -040038changes = ConfigChanges()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040039# Reload changed options in the following classes.
Tal Einat604e7b92018-09-25 15:10:14 +030040reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
41 Squeezer)
terryjreedy349abd92017-07-07 16:00:57 -040042
csabella5b591542017-07-28 14:40:59 -040043
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000044class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -040045 """Config dialog for IDLE.
46 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000047
Terry Jan Reedybfebfd82017-09-30 17:37:53 -040048 def __init__(self, parent, title='', *, _htest=False, _utest=False):
csabella7eb58832017-07-04 21:30:58 -040049 """Show the tabbed dialog for user configuration.
50
csabella36329a42017-07-13 23:32:01 -040051 Args:
52 parent - parent of this dialog
53 title - string which is the title of this popup dialog
54 _htest - bool, change box location when running htest
55 _utest - bool, don't wait_window when running unittest
56
57 Note: Focus set on font page fontlist.
58
59 Methods:
60 create_widgets
61 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040062 """
Steven M. Gavad721c482001-07-31 10:46:53 +000063 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040064 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040065 if _htest:
66 parent.instance_dict = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080067 if not _utest:
68 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000069
Terry Jan Reedycd567362014-10-17 01:31:35 -040070 self.title(title or 'IDLE Preferences')
csabellabac7d332017-06-26 17:46:26 -040071 x = parent.winfo_rootx() + 20
72 y = parent.winfo_rooty() + (30 if not _htest else 150)
73 self.geometry(f'+{x}+{y}')
csabella7eb58832017-07-04 21:30:58 -040074 # Each theme element key is its display name.
75 # The first value of the tuple is the sample area tag name.
76 # The second value is the display name list sort index.
csabellabac7d332017-06-26 17:46:26 -040077 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040078 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000079 self.transient(parent)
csabellabac7d332017-06-26 17:46:26 -040080 self.protocol("WM_DELETE_WINDOW", self.cancel)
csabella9397e2a2017-07-30 13:34:25 -040081 self.fontpage.fontlist.focus_set()
csabella7eb58832017-07-04 21:30:58 -040082 # XXX Decide whether to keep or delete these key bindings.
83 # Key bindings for this dialog.
84 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
85 # self.bind('<Alt-a>', self.Apply) #apply changes, save
86 # self.bind('<F1>', self.Help) #context help
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040087 # Attach callbacks after loading config to avoid calling them.
csabella5b591542017-07-28 14:40:59 -040088 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000089
Terry Jan Reedycfa89502014-07-14 23:07:32 -040090 if not _utest:
Louie Lu9b622fb2017-07-14 08:35:48 +080091 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040092 self.wm_deiconify()
93 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000094
csabellabac7d332017-06-26 17:46:26 -040095 def create_widgets(self):
csabella36329a42017-07-13 23:32:01 -040096 """Create and place widgets for tabbed dialog.
97
98 Widgets Bound to self:
Mark Rosemanc579ad12020-10-24 16:45:00 -070099 frame: encloses all other widgets
csabellae8eb17b2017-07-30 18:39:17 -0400100 note: Notebook
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400101 highpage: HighPage
csabellae8eb17b2017-07-30 18:39:17 -0400102 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400103 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -0400104 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400105 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -0400106
107 Methods:
csabella36329a42017-07-13 23:32:01 -0400108 create_action_buttons
109 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400110 activate_config_changes: Tell editors to reload.
111 """
Mark Rosemanc579ad12020-10-24 16:45:00 -0700112 self.frame = frame = Frame(self, padding="5px")
113 self.frame.grid(sticky="nwes")
114 self.note = note = Notebook(frame)
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400115 self.highpage = HighPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400116 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400117 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400118 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400119 self.extpage = self.create_page_extensions()
120 note.add(self.fontpage, text='Fonts/Tabs')
121 note.add(self.highpage, text='Highlights')
122 note.add(self.keyspage, text=' Keys ')
123 note.add(self.genpage, text=' General ')
124 note.add(self.extpage, text='Extensions')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400125 note.enable_traversal()
126 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400127 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400128
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400129 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -0400130 """Return frame of action buttons for dialog.
131
132 Methods:
133 ok
134 apply
135 cancel
136 help
137
138 Widget Structure:
139 outer: Frame
140 buttons: Frame
141 (no assignment): Button (ok)
142 (no assignment): Button (apply)
143 (no assignment): Button (cancel)
144 (no assignment): Button (help)
145 (no assignment): Frame
146 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400147 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400148 # Changing the default padding on OSX results in unreadable
csabella7eb58832017-07-04 21:30:58 -0400149 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400150 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000151 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400152 padding_args = {'padding': (6, 3)}
Mark Rosemanc579ad12020-10-24 16:45:00 -0700153 outer = Frame(self.frame, padding=2)
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500154 buttons_frame = Frame(outer, padding=2)
155 self.buttons = {}
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400156 for txt, cmd in (
csabellabac7d332017-06-26 17:46:26 -0400157 ('Ok', self.ok),
158 ('Apply', self.apply),
159 ('Cancel', self.cancel),
160 ('Help', self.help)):
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500161 self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd,
162 takefocus=FALSE, **padding_args)
163 self.buttons[txt].pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -0400164 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400165 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500166 buttons_frame.pack(side=BOTTOM)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400167 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400168
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400169 def ok(self):
170 """Apply config changes, then dismiss dialog.
171
172 Methods:
173 apply
174 destroy: inherited
175 """
176 self.apply()
177 self.destroy()
178
179 def apply(self):
180 """Apply config changes and leave dialog open.
181
182 Methods:
183 deactivate_current_config
184 save_all_changed_extensions
185 activate_config_changes
186 """
187 self.deactivate_current_config()
188 changes.save_all()
189 self.save_all_changed_extensions()
190 self.activate_config_changes()
191
192 def cancel(self):
193 """Dismiss config dialog.
194
195 Methods:
196 destroy: inherited
197 """
Cheryl Sabellad0d9fa82020-01-25 04:00:54 -0500198 changes.clear()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400199 self.destroy()
200
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300201 def destroy(self):
202 global font_sample_text
203 font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
Tal Einat10ea9402018-08-02 09:18:29 +0300204 self.grab_release()
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300205 super().destroy()
206
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400207 def help(self):
208 """Create textview for config dialog help.
209
Xtreakd9677f32019-06-03 09:51:15 +0530210 Attributes accessed:
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400211 note
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400212 Methods:
213 view_text: Method from textview module.
214 """
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400215 page = self.note.tab(self.note.select(), option='text').strip()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400216 view_text(self, title='Help for IDLE preferences',
Zackery Spytz2e43b642020-01-22 20:54:30 -0700217 contents=help_common+help_pages.get(page, ''))
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400218
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400219 def deactivate_current_config(self):
220 """Remove current key bindings.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400221 Iterate over window instances defined in parent and remove
222 the keybindings.
223 """
224 # Before a config is saved, some cleanup of current
225 # config must be done - remove the previous keybindings.
226 win_instances = self.parent.instance_dict.keys()
227 for instance in win_instances:
228 instance.RemoveKeybindings()
229
230 def activate_config_changes(self):
231 """Apply configuration changes to current windows.
232
233 Dynamically update the current parent window instances
234 with some of the configuration changes.
235 """
236 win_instances = self.parent.instance_dict.keys()
237 for instance in win_instances:
238 instance.ResetColorizer()
239 instance.ResetFont()
240 instance.set_notabs_indentwidth()
241 instance.ApplyKeybindings()
242 instance.reset_help_menu_entries()
Zackery Spytz9c284492019-11-13 00:13:33 -0700243 instance.update_cursor_blink()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -0400244 for klass in reloadables:
245 klass.reload()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400246
csabellabac7d332017-06-26 17:46:26 -0400247 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400248 """Part of the config dialog used for configuring IDLE extensions.
249
250 This code is generic - it works for any and all IDLE extensions.
251
252 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400253 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400254 GUI interface to change the configuration values, and saves the
255 changes using idleConf.
256
257 Not all changes take effect immediately - some may require restarting IDLE.
258 This depends on each extension's implementation.
259
260 All values are treated as text, and it is up to the user to supply
261 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300262 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -0400263
264 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300265 load_extensions:
csabella36329a42017-07-13 23:32:01 -0400266 extension_selected: Handle selection from list.
267 create_extension_frame: Hold widgets for one extension.
268 set_extension_value: Set in userCfg['extensions'].
269 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400270 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400271 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
272 self.ext_userCfg = idleConf.userCfg['extensions']
273 self.is_int = self.register(is_int)
274 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -0400275 # Create widgets - a listbox shows all available extensions, with the
276 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400277 self.extension_names = StringVar(self)
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -0700278 frame = Frame(self.note)
279 frame_ext = LabelFrame(frame, borderwidth=2, relief=GROOVE,
280 text=' Feature Extensions ')
281 frame_ext.rowconfigure(0, weight=1)
282 frame_ext.columnconfigure(2, weight=1)
283 self.extension_list = Listbox(frame_ext, listvariable=self.extension_names,
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400284 selectmode='browse')
285 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -0700286 scroll = Scrollbar(frame_ext, command=self.extension_list.yview)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400287 self.extension_list.yscrollcommand=scroll.set
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -0700288 self.details_frame = LabelFrame(frame_ext, width=250, height=250)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400289 self.extension_list.grid(column=0, row=0, sticky='nws')
290 scroll.grid(column=1, row=0, sticky='ns')
291 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -0700292 frame_ext.configure(padding=10)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400293 self.config_frame = {}
294 self.current_extension = None
295
296 self.outerframe = self # TEMPORARY
297 self.tabbed_page_set = self.extension_list # TEMPORARY
298
csabella7eb58832017-07-04 21:30:58 -0400299 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400300 ext_names = ''
301 for ext_name in sorted(self.extensions):
302 self.create_extension_frame(ext_name)
303 ext_names = ext_names + '{' + ext_name + '} '
304 self.extension_names.set(ext_names)
305 self.extension_list.selection_set(0)
306 self.extension_selected(None)
307
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -0700308
309 self.frame_help = HelpFrame(frame, borderwidth=2, relief=GROOVE,
310 text=' Help Menu Extensions ')
311 frame_ext.grid(row=0, column=0, sticky='nsew')
312 Label(frame).grid(row=1, column=0)
313 self.frame_help.grid(row=2, column=0, sticky='sew')
314
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400315 return frame
316
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400317 def load_extensions(self):
318 "Fill self.extensions with data from the default and user configs."
319 self.extensions = {}
320 for ext_name in idleConf.GetExtensions(active_only=False):
wohlganger58fc71c2017-09-10 16:19:47 -0500321 # Former built-in extensions are already filtered out.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400322 self.extensions[ext_name] = []
323
324 for ext_name in self.extensions:
325 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
326
csabella7eb58832017-07-04 21:30:58 -0400327 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400328 enables = [opt_name for opt_name in opt_list
329 if opt_name.startswith('enable')]
330 for opt_name in enables:
331 opt_list.remove(opt_name)
332 opt_list = enables + opt_list
333
334 for opt_name in opt_list:
335 def_str = self.ext_defaultCfg.Get(
336 ext_name, opt_name, raw=True)
337 try:
338 def_obj = {'True':True, 'False':False}[def_str]
339 opt_type = 'bool'
340 except KeyError:
341 try:
342 def_obj = int(def_str)
343 opt_type = 'int'
344 except ValueError:
345 def_obj = def_str
346 opt_type = None
347 try:
348 value = self.ext_userCfg.Get(
349 ext_name, opt_name, type=opt_type, raw=True,
350 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -0400351 except ValueError: # Need this until .Get fixed.
352 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400353 var = StringVar(self)
354 var.set(str(value))
355
356 self.extensions[ext_name].append({'name': opt_name,
357 'type': opt_type,
358 'default': def_str,
359 'value': value,
360 'var': var,
361 })
362
363 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -0400364 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400365 newsel = self.extension_list.curselection()
366 if newsel:
367 newsel = self.extension_list.get(newsel)
368 if newsel is None or newsel != self.current_extension:
369 if self.current_extension:
370 self.details_frame.config(text='')
371 self.config_frame[self.current_extension].grid_forget()
372 self.current_extension = None
373 if newsel:
374 self.details_frame.config(text=newsel)
375 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
376 self.current_extension = newsel
377
378 def create_extension_frame(self, ext_name):
379 """Create a frame holding the widgets to configure one extension"""
380 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
381 self.config_frame[ext_name] = f
382 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -0400383 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400384 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -0400385 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400386 label = Label(entry_area, text=opt['name'])
387 label.grid(row=row, column=0, sticky=NW)
388 var = opt['var']
389 if opt['type'] == 'bool':
Cheryl Sabella7028e592017-08-26 14:26:02 -0400390 Checkbutton(entry_area, variable=var,
391 onvalue='True', offvalue='False', width=8
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400392 ).grid(row=row, column=1, sticky=W, padx=7)
393 elif opt['type'] == 'int':
394 Entry(entry_area, textvariable=var, validate='key',
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400395 validatecommand=(self.is_int, '%P'), width=10
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400396 ).grid(row=row, column=1, sticky=NSEW, padx=7)
397
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400398 else: # type == 'str'
399 # Limit size to fit non-expanding space with larger font.
400 Entry(entry_area, textvariable=var, width=15
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400401 ).grid(row=row, column=1, sticky=NSEW, padx=7)
402 return
403
404 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400405 """Return True if the configuration was added or changed.
406
407 If the value is the same as the default, then remove it
408 from user config file.
409 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400410 name = opt['name']
411 default = opt['default']
412 value = opt['var'].get().strip() or default
413 opt['var'].set(value)
414 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400415 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400416 if (value == default):
417 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400418 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400419 return self.ext_userCfg.SetOption(section, name, value)
420
421 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400422 """Save configuration changes to the user config file.
423
424 Attributes accessed:
425 extensions
426
427 Methods:
428 set_extension_value
429 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400430 has_changes = False
431 for ext_name in self.extensions:
432 options = self.extensions[ext_name]
433 for opt in options:
434 if self.set_extension_value(ext_name, opt):
435 has_changes = True
436 if has_changes:
437 self.ext_userCfg.Save()
438
439
csabella6f446be2017-08-01 00:24:07 -0400440# class TabPage(Frame): # A template for Page classes.
441# def __init__(self, master):
442# super().__init__(master)
443# self.create_page_tab()
444# self.load_tab_cfg()
445# def create_page_tab(self):
446# # Define tk vars and register var and callback with tracers.
447# # Create subframes and widgets.
448# # Pack widgets.
449# def load_tab_cfg(self):
450# # Initialize widgets with data from idleConf.
451# def var_changed_var_name():
452# # For each tk var that needs other than default callback.
453# def other_methods():
454# # Define tab-specific behavior.
455
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300456font_sample_text = (
457 '<ASCII/Latin1>\n'
458 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
459 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
460 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
461 '\n<IPA,Greek,Cyrillic>\n'
462 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
463 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
464 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
465 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
466 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
467 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
468 '\n<Hebrew, Arabic>\n'
469 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
470 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
471 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
472 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
473 '\n<Devanagari, Tamil>\n'
474 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
475 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
476 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
477 '\u0b85\u0b87\u0b89\u0b8e\n'
478 '\n<East Asian>\n'
479 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
480 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
481 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
482 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
483 )
484
csabella6f446be2017-08-01 00:24:07 -0400485
csabella9397e2a2017-07-30 13:34:25 -0400486class FontPage(Frame):
487
csabella6f446be2017-08-01 00:24:07 -0400488 def __init__(self, master, highpage):
489 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400490 self.highlight_sample = highpage.highlight_sample
491 self.create_page_font_tab()
492 self.load_font_cfg()
493 self.load_tab_cfg()
494
495 def create_page_font_tab(self):
496 """Return frame of widgets for Font/Tabs tab.
497
498 Fonts: Enable users to provisionally change font face, size, or
499 boldness and to see the consequence of proposed choices. Each
500 action set 3 options in changes structuree and changes the
501 corresponding aspect of the font sample on this page and
502 highlight sample on highlight page.
503
504 Function load_font_cfg initializes font vars and widgets from
505 idleConf entries and tk.
506
507 Fontlist: mouse button 1 click or up or down key invoke
508 on_fontlist_select(), which sets var font_name.
509
510 Sizelist: clicking the menubutton opens the dropdown menu. A
511 mouse button 1 click or return key sets var font_size.
512
513 Bold_toggle: clicking the box toggles var font_bold.
514
515 Changing any of the font vars invokes var_changed_font, which
516 adds all 3 font options to changes and calls set_samples.
517 Set_samples applies a new font constructed from the font vars to
Leo Ariasc3d95082018-02-03 18:36:10 -0600518 font_sample and to highlight_sample on the highlight page.
csabella9397e2a2017-07-30 13:34:25 -0400519
520 Tabs: Enable users to change spaces entered for indent tabs.
521 Changing indent_scale value with the mouse sets Var space_num,
522 which invokes the default callback to add an entry to
523 changes. Load_tab_cfg initializes space_num to default.
524
Cheryl Sabella2f896462017-08-14 21:21:43 -0400525 Widgets for FontPage(Frame): (*) widgets bound to self
526 frame_font: LabelFrame
527 frame_font_name: Frame
528 font_name_title: Label
529 (*)fontlist: ListBox - font_name
530 scroll_font: Scrollbar
531 frame_font_param: Frame
532 font_size_title: Label
533 (*)sizelist: DynOptionMenu - font_size
534 (*)bold_toggle: Checkbutton - font_bold
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400535 frame_sample: LabelFrame
536 (*)font_sample: Label
Cheryl Sabella2f896462017-08-14 21:21:43 -0400537 frame_indent: LabelFrame
538 indent_title: Label
539 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400540 """
csabella6f446be2017-08-01 00:24:07 -0400541 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
542 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
543 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400544 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
545
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400546 # Define frames and widgets.
csabella9397e2a2017-07-30 13:34:25 -0400547 frame_font = LabelFrame(
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400548 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
549 frame_sample = LabelFrame(
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300550 self, borderwidth=2, relief=GROOVE,
551 text=' Font Sample (Editable) ')
csabella9397e2a2017-07-30 13:34:25 -0400552 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400553 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400554 # frame_font.
555 frame_font_name = Frame(frame_font)
556 frame_font_param = Frame(frame_font)
557 font_name_title = Label(
558 frame_font_name, justify=LEFT, text='Font Face :')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400559 self.fontlist = Listbox(frame_font_name, height=15,
csabella9397e2a2017-07-30 13:34:25 -0400560 takefocus=True, exportselection=FALSE)
561 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
562 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
563 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
564 scroll_font = Scrollbar(frame_font_name)
565 scroll_font.config(command=self.fontlist.yview)
566 self.fontlist.config(yscrollcommand=scroll_font.set)
567 font_size_title = Label(frame_font_param, text='Size :')
568 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
569 self.bold_toggle = Checkbutton(
570 frame_font_param, variable=self.font_bold,
571 onvalue=1, offvalue=0, text='Bold')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400572 # frame_sample.
Tal Einat3221a632019-07-27 19:57:48 +0300573 font_sample_frame = ScrollableTextFrame(frame_sample)
574 self.font_sample = font_sample_frame.text
575 self.font_sample.config(wrap=NONE, width=1, height=1)
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300576 self.font_sample.insert(END, font_sample_text)
csabella9397e2a2017-07-30 13:34:25 -0400577 # frame_indent.
578 indent_title = Label(
579 frame_indent, justify=LEFT,
580 text='Python Standard: 4 Spaces!')
581 self.indent_scale = Scale(
582 frame_indent, variable=self.space_num,
583 orient='horizontal', tickinterval=2, from_=2, to=16)
584
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400585 # Grid and pack widgets:
586 self.columnconfigure(1, weight=1)
Tal Einat3221a632019-07-27 19:57:48 +0300587 self.rowconfigure(2, weight=1)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400588 frame_font.grid(row=0, column=0, padx=5, pady=5)
Tal Einat3221a632019-07-27 19:57:48 +0300589 frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5,
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400590 sticky='nsew')
591 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
csabella9397e2a2017-07-30 13:34:25 -0400592 # frame_font.
593 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
594 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
595 font_name_title.pack(side=TOP, anchor=W)
596 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
597 scroll_font.pack(side=LEFT, fill=Y)
598 font_size_title.pack(side=LEFT, anchor=W)
599 self.sizelist.pack(side=LEFT, anchor=W)
600 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400601 # frame_sample.
Tal Einat3221a632019-07-27 19:57:48 +0300602 font_sample_frame.pack(expand=TRUE, fill=BOTH)
csabella9397e2a2017-07-30 13:34:25 -0400603 # frame_indent.
csabella9397e2a2017-07-30 13:34:25 -0400604 indent_title.pack(side=TOP, anchor=W, padx=5)
605 self.indent_scale.pack(side=TOP, padx=5, fill=X)
606
csabella9397e2a2017-07-30 13:34:25 -0400607 def load_font_cfg(self):
608 """Load current configuration settings for the font options.
609
610 Retrieve current font with idleConf.GetFont and font families
611 from tk. Setup fontlist and set font_name. Setup sizelist,
612 which sets font_size. Set font_bold. Call set_samples.
613 """
614 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
615 font_name = configured_font[0].lower()
616 font_size = configured_font[1]
617 font_bold = configured_font[2]=='bold'
618
Terry Jan Reedy96ce2272020-02-10 20:08:58 -0500619 # Set sorted no-duplicate editor font selection list and font_name.
Terry Jan Reedy879986d2021-01-25 06:33:18 -0500620 fonts = sorted(set(tkfont.families(self)))
csabella9397e2a2017-07-30 13:34:25 -0400621 for font in fonts:
622 self.fontlist.insert(END, font)
623 self.font_name.set(font_name)
624 lc_fonts = [s.lower() for s in fonts]
625 try:
626 current_font_index = lc_fonts.index(font_name)
627 self.fontlist.see(current_font_index)
628 self.fontlist.select_set(current_font_index)
629 self.fontlist.select_anchor(current_font_index)
630 self.fontlist.activate(current_font_index)
631 except ValueError:
632 pass
633 # Set font size dropdown.
634 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
635 '16', '18', '20', '22', '25', '29', '34', '40'),
636 font_size)
637 # Set font weight.
638 self.font_bold.set(font_bold)
639 self.set_samples()
640
641 def var_changed_font(self, *params):
642 """Store changes to font attributes.
643
644 When one font attribute changes, save them all, as they are
645 not independent from each other. In particular, when we are
646 overriding the default font, we need to write out everything.
647 """
648 value = self.font_name.get()
649 changes.add_option('main', 'EditorWindow', 'font', value)
650 value = self.font_size.get()
651 changes.add_option('main', 'EditorWindow', 'font-size', value)
652 value = self.font_bold.get()
653 changes.add_option('main', 'EditorWindow', 'font-bold', value)
654 self.set_samples()
655
656 def on_fontlist_select(self, event):
657 """Handle selecting a font from the list.
658
659 Event can result from either mouse click or Up or Down key.
660 Set font_name and example displays to selection.
661 """
662 font = self.fontlist.get(
663 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
664 self.font_name.set(font.lower())
665
666 def set_samples(self, event=None):
667 """Update update both screen samples with the font settings.
668
669 Called on font initialization and change events.
670 Accesses font_name, font_size, and font_bold Variables.
Leo Ariasc3d95082018-02-03 18:36:10 -0600671 Updates font_sample and highlight page highlight_sample.
csabella9397e2a2017-07-30 13:34:25 -0400672 """
673 font_name = self.font_name.get()
Terry Jan Reedy879986d2021-01-25 06:33:18 -0500674 font_weight = tkfont.BOLD if self.font_bold.get() else tkfont.NORMAL
csabella9397e2a2017-07-30 13:34:25 -0400675 new_font = (font_name, self.font_size.get(), font_weight)
676 self.font_sample['font'] = new_font
677 self.highlight_sample['font'] = new_font
678
679 def load_tab_cfg(self):
680 """Load current configuration settings for the tab options.
681
682 Attributes updated:
683 space_num: Set to value from idleConf.
684 """
685 # Set indent sizes.
686 space_num = idleConf.GetOption(
687 'main', 'Indent', 'num-spaces', default=4, type='int')
688 self.space_num.set(space_num)
689
690 def var_changed_space_num(self, *params):
691 "Store change to indentation size."
692 value = self.space_num.get()
693 changes.add_option('main', 'Indent', 'num-spaces', value)
694
695
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400696class HighPage(Frame):
697
698 def __init__(self, master):
699 super().__init__(master)
Mark Rosemanc579ad12020-10-24 16:45:00 -0700700 self.cd = master.winfo_toplevel()
Cheryl Sabella7028e592017-08-26 14:26:02 -0400701 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400702 self.create_page_highlight()
703 self.load_theme_cfg()
704
705 def create_page_highlight(self):
706 """Return frame of widgets for Highlighting tab.
707
708 Enable users to provisionally change foreground and background
709 colors applied to textual tags. Color mappings are stored in
710 complete listings called themes. Built-in themes in
711 idlelib/config-highlight.def are fixed as far as the dialog is
712 concerned. Any theme can be used as the base for a new custom
713 theme, stored in .idlerc/config-highlight.cfg.
714
715 Function load_theme_cfg() initializes tk variables and theme
716 lists and calls paint_theme_sample() and set_highlight_target()
717 for the current theme. Radiobuttons builtin_theme_on and
718 custom_theme_on toggle var theme_source, which controls if the
719 current set of colors are from a builtin or custom theme.
720 DynOptionMenus builtinlist and customlist contain lists of the
721 builtin and custom themes, respectively, and the current item
722 from each list is stored in vars builtin_name and custom_name.
723
724 Function paint_theme_sample() applies the colors from the theme
725 to the tags in text widget highlight_sample and then invokes
726 set_color_sample(). Function set_highlight_target() sets the state
727 of the radiobuttons fg_on and bg_on based on the tag and it also
728 invokes set_color_sample().
729
730 Function set_color_sample() sets the background color for the frame
731 holding the color selector. This provides a larger visual of the
732 color for the current tag and plane (foreground/background).
733
734 Note: set_color_sample() is called from many places and is often
735 called more than once when a change is made. It is invoked when
736 foreground or background is selected (radiobuttons), from
737 paint_theme_sample() (theme is changed or load_cfg is called), and
738 from set_highlight_target() (target tag is changed or load_cfg called).
739
740 Button delete_custom invokes delete_custom() to delete
741 a custom theme from idleConf.userCfg['highlight'] and changes.
742 Button save_custom invokes save_as_new_theme() which calls
743 get_new_theme_name() and create_new() to save a custom theme
744 and its colors to idleConf.userCfg['highlight'].
745
746 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
747 if the current selected color for a tag is for the foreground or
748 background.
749
750 DynOptionMenu targetlist contains a readable description of the
751 tags applied to Python source within IDLE. Selecting one of the
752 tags from this list populates highlight_target, which has a callback
753 function set_highlight_target().
754
755 Text widget highlight_sample displays a block of text (which is
756 mock Python code) in which is embedded the defined tags and reflects
757 the color attributes of the current theme and changes for those tags.
758 Mouse button 1 allows for selection of a tag and updates
759 highlight_target with that tag value.
760
761 Note: The font in highlight_sample is set through the config in
762 the fonts tab.
763
764 In other words, a tag can be selected either from targetlist or
765 by clicking on the sample text within highlight_sample. The
766 plane (foreground/background) is selected via the radiobutton.
767 Together, these two (tag and plane) control what color is
768 shown in set_color_sample() for the current theme. Button set_color
769 invokes get_color() which displays a ColorChooser to change the
770 color for the selected tag/plane. If a new color is picked,
771 it will be saved to changes and the highlight_sample and
772 frame background will be updated.
773
774 Tk Variables:
775 color: Color of selected target.
776 builtin_name: Menu variable for built-in theme.
777 custom_name: Menu variable for custom theme.
778 fg_bg_toggle: Toggle for foreground/background color.
779 Note: this has no callback.
780 theme_source: Selector for built-in or custom theme.
781 highlight_target: Menu variable for the highlight tag target.
782
783 Instance Data Attributes:
784 theme_elements: Dictionary of tags for text highlighting.
785 The key is the display name and the value is a tuple of
786 (tag name, display sort order).
787
788 Methods [attachment]:
789 load_theme_cfg: Load current highlight colors.
790 get_color: Invoke colorchooser [button_set_color].
791 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
792 set_highlight_target: set fg_bg_toggle, set_color_sample().
793 set_color_sample: Set frame background to target.
794 on_new_color_set: Set new color and add option.
795 paint_theme_sample: Recolor sample.
796 get_new_theme_name: Get from popup.
797 create_new: Combine theme with changes and save.
798 save_as_new_theme: Save [button_save_custom].
799 set_theme_type: Command for [theme_source].
800 delete_custom: Activate default [button_delete_custom].
801 save_new: Save to userCfg['theme'] (is function).
802
803 Widgets of highlights page frame: (*) widgets bound to self
804 frame_custom: LabelFrame
805 (*)highlight_sample: Text
806 (*)frame_color_set: Frame
807 (*)button_set_color: Button
808 (*)targetlist: DynOptionMenu - highlight_target
809 frame_fg_bg_toggle: Frame
810 (*)fg_on: Radiobutton - fg_bg_toggle
811 (*)bg_on: Radiobutton - fg_bg_toggle
812 (*)button_save_custom: Button
813 frame_theme: LabelFrame
814 theme_type_title: Label
815 (*)builtin_theme_on: Radiobutton - theme_source
816 (*)custom_theme_on: Radiobutton - theme_source
817 (*)builtinlist: DynOptionMenu - builtin_name
818 (*)customlist: DynOptionMenu - custom_name
819 (*)button_delete_custom: Button
820 (*)theme_message: Label
821 """
822 self.theme_elements = {
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400823 'Normal Code or Text': ('normal', '00'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400824 'Code Context': ('context', '01'),
825 'Python Keywords': ('keyword', '02'),
826 'Python Definitions': ('definition', '03'),
827 'Python Builtins': ('builtin', '04'),
828 'Python Comments': ('comment', '05'),
829 'Python Strings': ('string', '06'),
830 'Selected Text': ('hilite', '07'),
831 'Found Text': ('hit', '08'),
832 'Cursor': ('cursor', '09'),
833 'Editor Breakpoint': ('break', '10'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400834 'Shell Prompt': ('console', '11'),
835 'Error Text': ('error', '12'),
836 'Shell User Output': ('stdout', '13'),
837 'Shell User Exception': ('stderr', '14'),
Tal Einat7123ea02019-07-23 15:22:11 +0300838 'Line Number': ('linenumber', '16'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400839 }
840 self.builtin_name = tracers.add(
841 StringVar(self), self.var_changed_builtin_name)
842 self.custom_name = tracers.add(
843 StringVar(self), self.var_changed_custom_name)
844 self.fg_bg_toggle = BooleanVar(self)
845 self.color = tracers.add(
846 StringVar(self), self.var_changed_color)
847 self.theme_source = tracers.add(
848 BooleanVar(self), self.var_changed_theme_source)
849 self.highlight_target = tracers.add(
850 StringVar(self), self.var_changed_highlight_target)
851
852 # Create widgets:
853 # body frame and section frames.
854 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
855 text=' Custom Highlighting ')
856 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
857 text=' Highlighting Theme ')
858 # frame_custom.
Tal Einat3221a632019-07-27 19:57:48 +0300859 sample_frame = ScrollableTextFrame(
860 frame_custom, relief=SOLID, borderwidth=1)
861 text = self.highlight_sample = sample_frame.text
862 text.configure(
863 font=('courier', 12, ''), cursor='hand2', width=1, height=1,
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400864 takefocus=FALSE, highlightthickness=0, wrap=NONE)
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500865 # Prevent perhaps invisible selection of word or slice.
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400866 text.bind('<Double-Button-1>', lambda e: 'break')
867 text.bind('<B1-Motion>', lambda e: 'break')
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400868 string_tags=(
869 ('# Click selects item.', 'comment'), ('\n', 'normal'),
870 ('code context section', 'context'), ('\n', 'normal'),
871 ('| cursor', 'cursor'), ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400872 ('def', 'keyword'), (' ', 'normal'),
873 ('func', 'definition'), ('(param):\n ', 'normal'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400874 ('"Return None."', 'string'), ('\n var0 = ', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400875 ("'string'", 'string'), ('\n var1 = ', 'normal'),
876 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
877 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
878 ('list', 'builtin'), ('(', 'normal'),
879 ('None', 'keyword'), (')\n', 'normal'),
880 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400881 ('>>>', 'console'), (' 3.14**2\n', 'normal'),
882 ('9.8596', 'stdout'), ('\n', 'normal'),
883 ('>>>', 'console'), (' pri ', 'normal'),
884 ('n', 'error'), ('t(\n', 'normal'),
885 ('SyntaxError', 'stderr'), ('\n', 'normal'))
886 for string, tag in string_tags:
887 text.insert(END, string, tag)
Tal Einat7123ea02019-07-23 15:22:11 +0300888 n_lines = len(text.get('1.0', END).splitlines())
Tal Einat3221a632019-07-27 19:57:48 +0300889 for lineno in range(1, n_lines):
Tal Einat7123ea02019-07-23 15:22:11 +0300890 text.insert(f'{lineno}.0',
891 f'{lineno:{len(str(n_lines))}d} ',
892 'linenumber')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400893 for element in self.theme_elements:
894 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400895 # event.widget.winfo_top_level().highlight_target.set(elem)
896 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400897 text.tag_bind(
898 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400899 text['state'] = 'disabled'
900 self.style.configure('frame_color_set.TFrame', borderwidth=1,
901 relief='solid')
902 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400903 frame_fg_bg_toggle = Frame(frame_custom)
904 self.button_set_color = Button(
905 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400906 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400907 self.targetlist = DynOptionMenu(
908 self.frame_color_set, self.highlight_target, None,
909 highlightthickness=0) #, command=self.set_highlight_targetBinding
910 self.fg_on = Radiobutton(
911 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
912 text='Foreground', command=self.set_color_sample_binding)
913 self.bg_on = Radiobutton(
914 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
915 text='Background', command=self.set_color_sample_binding)
916 self.fg_bg_toggle.set(1)
917 self.button_save_custom = Button(
918 frame_custom, text='Save as New Custom Theme',
919 command=self.save_as_new_theme)
920 # frame_theme.
921 theme_type_title = Label(frame_theme, text='Select : ')
922 self.builtin_theme_on = Radiobutton(
923 frame_theme, variable=self.theme_source, value=1,
924 command=self.set_theme_type, text='a Built-in Theme')
925 self.custom_theme_on = Radiobutton(
926 frame_theme, variable=self.theme_source, value=0,
927 command=self.set_theme_type, text='a Custom Theme')
928 self.builtinlist = DynOptionMenu(
929 frame_theme, self.builtin_name, None, command=None)
930 self.customlist = DynOptionMenu(
931 frame_theme, self.custom_name, None, command=None)
932 self.button_delete_custom = Button(
933 frame_theme, text='Delete Custom Theme',
934 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400935 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400936 # Pack widgets:
937 # body.
938 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500939 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400940 # frame_custom.
Tal Einat3221a632019-07-27 19:57:48 +0300941 self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400942 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
Tal Einat3221a632019-07-27 19:57:48 +0300943 sample_frame.pack(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400944 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
945 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
946 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
947 self.fg_on.pack(side=LEFT, anchor=E)
948 self.bg_on.pack(side=RIGHT, anchor=W)
949 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
950 # frame_theme.
951 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
952 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
953 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
954 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
955 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
956 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
957 self.theme_message.pack(side=TOP, fill=X, pady=5)
958
959 def load_theme_cfg(self):
960 """Load current configuration settings for the theme options.
961
962 Based on the theme_source toggle, the theme is set as
963 either builtin or custom and the initial widget values
964 reflect the current settings from idleConf.
965
966 Attributes updated:
967 theme_source: Set from idleConf.
968 builtinlist: List of default themes from idleConf.
969 customlist: List of custom themes from idleConf.
970 custom_theme_on: Disabled if there are no custom themes.
971 custom_theme: Message with additional information.
972 targetlist: Create menu from self.theme_elements.
973
974 Methods:
975 set_theme_type
976 paint_theme_sample
977 set_highlight_target
978 """
979 # Set current theme type radiobutton.
980 self.theme_source.set(idleConf.GetOption(
981 'main', 'Theme', 'default', type='bool', default=1))
982 # Set current theme.
983 current_option = idleConf.CurrentTheme()
984 # Load available theme option menus.
985 if self.theme_source.get(): # Default theme selected.
986 item_list = idleConf.GetSectionList('default', 'highlight')
987 item_list.sort()
988 self.builtinlist.SetMenu(item_list, current_option)
989 item_list = idleConf.GetSectionList('user', 'highlight')
990 item_list.sort()
991 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400992 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400993 self.custom_name.set('- no custom themes -')
994 else:
995 self.customlist.SetMenu(item_list, item_list[0])
996 else: # User theme selected.
997 item_list = idleConf.GetSectionList('user', 'highlight')
998 item_list.sort()
999 self.customlist.SetMenu(item_list, current_option)
1000 item_list = idleConf.GetSectionList('default', 'highlight')
1001 item_list.sort()
1002 self.builtinlist.SetMenu(item_list, item_list[0])
1003 self.set_theme_type()
1004 # Load theme element option menu.
1005 theme_names = list(self.theme_elements.keys())
1006 theme_names.sort(key=lambda x: self.theme_elements[x][1])
1007 self.targetlist.SetMenu(theme_names, theme_names[0])
1008 self.paint_theme_sample()
1009 self.set_highlight_target()
1010
1011 def var_changed_builtin_name(self, *params):
1012 """Process new builtin theme selection.
1013
1014 Add the changed theme's name to the changed_items and recreate
1015 the sample with the values from the selected theme.
1016 """
1017 old_themes = ('IDLE Classic', 'IDLE New')
1018 value = self.builtin_name.get()
1019 if value not in old_themes:
1020 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
1021 changes.add_option('main', 'Theme', 'name', old_themes[0])
1022 changes.add_option('main', 'Theme', 'name2', value)
1023 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001024 else:
1025 changes.add_option('main', 'Theme', 'name', value)
1026 changes.add_option('main', 'Theme', 'name2', '')
1027 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001028 self.paint_theme_sample()
1029
1030 def var_changed_custom_name(self, *params):
1031 """Process new custom theme selection.
1032
1033 If a new custom theme is selected, add the name to the
1034 changed_items and apply the theme to the sample.
1035 """
1036 value = self.custom_name.get()
1037 if value != '- no custom themes -':
1038 changes.add_option('main', 'Theme', 'name', value)
1039 self.paint_theme_sample()
1040
1041 def var_changed_theme_source(self, *params):
1042 """Process toggle between builtin and custom theme.
1043
1044 Update the default toggle value and apply the newly
1045 selected theme type.
1046 """
1047 value = self.theme_source.get()
1048 changes.add_option('main', 'Theme', 'default', value)
1049 if value:
1050 self.var_changed_builtin_name()
1051 else:
1052 self.var_changed_custom_name()
1053
1054 def var_changed_color(self, *params):
1055 "Process change to color choice."
1056 self.on_new_color_set()
1057
1058 def var_changed_highlight_target(self, *params):
1059 "Process selection of new target tag for highlighting."
1060 self.set_highlight_target()
1061
1062 def set_theme_type(self):
1063 """Set available screen options based on builtin or custom theme.
1064
1065 Attributes accessed:
1066 theme_source
1067
1068 Attributes updated:
1069 builtinlist
1070 customlist
1071 button_delete_custom
1072 custom_theme_on
1073
1074 Called from:
1075 handler for builtin_theme_on and custom_theme_on
1076 delete_custom
1077 create_new
1078 load_theme_cfg
1079 """
1080 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001081 self.builtinlist['state'] = 'normal'
1082 self.customlist['state'] = 'disabled'
1083 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001084 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001085 self.builtinlist['state'] = 'disabled'
1086 self.custom_theme_on.state(('!disabled',))
1087 self.customlist['state'] = 'normal'
1088 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001089
1090 def get_color(self):
1091 """Handle button to select a new color for the target tag.
1092
1093 If a new color is selected while using a builtin theme, a
1094 name must be supplied to create a custom theme.
1095
1096 Attributes accessed:
1097 highlight_target
1098 frame_color_set
1099 theme_source
1100
1101 Attributes updated:
1102 color
1103
1104 Methods:
1105 get_new_theme_name
1106 create_new
1107 """
1108 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001109 prev_color = self.style.lookup(self.frame_color_set['style'],
1110 'background')
Terry Jan Reedy879986d2021-01-25 06:33:18 -05001111 rgbTuplet, color_string = colorchooser.askcolor(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001112 parent=self, title='Pick new color for : '+target,
1113 initialcolor=prev_color)
1114 if color_string and (color_string != prev_color):
1115 # User didn't cancel and they chose a new color.
1116 if self.theme_source.get(): # Current theme is a built-in.
1117 message = ('Your changes will be saved as a new Custom Theme. '
1118 'Enter a name for your new Custom Theme below.')
1119 new_theme = self.get_new_theme_name(message)
1120 if not new_theme: # User cancelled custom theme creation.
1121 return
1122 else: # Create new custom theme based on previously active theme.
1123 self.create_new(new_theme)
1124 self.color.set(color_string)
1125 else: # Current theme is user defined.
1126 self.color.set(color_string)
1127
1128 def on_new_color_set(self):
1129 "Display sample of new color selection on the dialog."
1130 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001131 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001132 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1133 sample_element = self.theme_elements[self.highlight_target.get()][0]
1134 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1135 theme = self.custom_name.get()
1136 theme_element = sample_element + '-' + plane
1137 changes.add_option('highlight', theme, theme_element, new_color)
1138
1139 def get_new_theme_name(self, message):
1140 "Return name of new theme from query popup."
1141 used_names = (idleConf.GetSectionList('user', 'highlight') +
1142 idleConf.GetSectionList('default', 'highlight'))
1143 new_theme = SectionName(
1144 self, 'New Custom Theme', message, used_names).result
1145 return new_theme
1146
1147 def save_as_new_theme(self):
1148 """Prompt for new theme name and create the theme.
1149
1150 Methods:
1151 get_new_theme_name
1152 create_new
1153 """
1154 new_theme_name = self.get_new_theme_name('New Theme Name:')
1155 if new_theme_name:
1156 self.create_new(new_theme_name)
1157
1158 def create_new(self, new_theme_name):
1159 """Create a new custom theme with the given name.
1160
1161 Create the new theme based on the previously active theme
1162 with the current changes applied. Once it is saved, then
1163 activate the new theme.
1164
1165 Attributes accessed:
1166 builtin_name
1167 custom_name
1168
1169 Attributes updated:
1170 customlist
1171 theme_source
1172
1173 Method:
1174 save_new
1175 set_theme_type
1176 """
1177 if self.theme_source.get():
1178 theme_type = 'default'
1179 theme_name = self.builtin_name.get()
1180 else:
1181 theme_type = 'user'
1182 theme_name = self.custom_name.get()
1183 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1184 # Apply any of the old theme's unsaved changes to the new theme.
1185 if theme_name in changes['highlight']:
1186 theme_changes = changes['highlight'][theme_name]
1187 for element in theme_changes:
1188 new_theme[element] = theme_changes[element]
1189 # Save the new theme.
1190 self.save_new(new_theme_name, new_theme)
1191 # Change GUI over to the new theme.
1192 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1193 custom_theme_list.sort()
1194 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1195 self.theme_source.set(0)
1196 self.set_theme_type()
1197
1198 def set_highlight_target(self):
1199 """Set fg/bg toggle and color based on highlight tag target.
1200
1201 Instance variables accessed:
1202 highlight_target
1203
1204 Attributes updated:
1205 fg_on
1206 bg_on
1207 fg_bg_toggle
1208
1209 Methods:
1210 set_color_sample
1211
1212 Called from:
1213 var_changed_highlight_target
1214 load_theme_cfg
1215 """
1216 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001217 self.fg_on.state(('disabled',))
1218 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001219 self.fg_bg_toggle.set(1)
1220 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001221 self.fg_on.state(('!disabled',))
1222 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001223 self.fg_bg_toggle.set(1)
1224 self.set_color_sample()
1225
1226 def set_color_sample_binding(self, *args):
1227 """Change color sample based on foreground/background toggle.
1228
1229 Methods:
1230 set_color_sample
1231 """
1232 self.set_color_sample()
1233
1234 def set_color_sample(self):
1235 """Set the color of the frame background to reflect the selected target.
1236
1237 Instance variables accessed:
1238 theme_elements
1239 highlight_target
1240 fg_bg_toggle
1241 highlight_sample
1242
1243 Attributes updated:
1244 frame_color_set
1245 """
1246 # Set the color sample area.
1247 tag = self.theme_elements[self.highlight_target.get()][0]
1248 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1249 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001250 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001251
1252 def paint_theme_sample(self):
1253 """Apply the theme colors to each element tag in the sample text.
1254
1255 Instance attributes accessed:
1256 theme_elements
1257 theme_source
1258 builtin_name
1259 custom_name
1260
1261 Attributes updated:
1262 highlight_sample: Set the tag elements to the theme.
1263
1264 Methods:
1265 set_color_sample
1266
1267 Called from:
1268 var_changed_builtin_name
1269 var_changed_custom_name
1270 load_theme_cfg
1271 """
1272 if self.theme_source.get(): # Default theme
1273 theme = self.builtin_name.get()
1274 else: # User theme
1275 theme = self.custom_name.get()
1276 for element_title in self.theme_elements:
1277 element = self.theme_elements[element_title][0]
1278 colors = idleConf.GetHighlight(theme, element)
1279 if element == 'cursor': # Cursor sample needs special painting.
1280 colors['background'] = idleConf.GetHighlight(
Terry Jan Reedyc1419572019-03-22 18:23:41 -04001281 theme, 'normal')['background']
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001282 # Handle any unsaved changes to this theme.
1283 if theme in changes['highlight']:
1284 theme_dict = changes['highlight'][theme]
1285 if element + '-foreground' in theme_dict:
1286 colors['foreground'] = theme_dict[element + '-foreground']
1287 if element + '-background' in theme_dict:
1288 colors['background'] = theme_dict[element + '-background']
1289 self.highlight_sample.tag_config(element, **colors)
1290 self.set_color_sample()
1291
1292 def save_new(self, theme_name, theme):
1293 """Save a newly created theme to idleConf.
1294
1295 theme_name - string, the name of the new theme
1296 theme - dictionary containing the new theme
1297 """
Cheryl Sabelladd023ad2020-01-27 17:15:56 -05001298 idleConf.userCfg['highlight'].AddSection(theme_name)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001299 for element in theme:
1300 value = theme[element]
1301 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1302
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001303 def askyesno(self, *args, **kwargs):
1304 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001305 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001306
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001307 def delete_custom(self):
1308 """Handle event to delete custom theme.
1309
1310 The current theme is deactivated and the default theme is
1311 activated. The custom theme is permanently removed from
1312 the config file.
1313
1314 Attributes accessed:
1315 custom_name
1316
1317 Attributes updated:
1318 custom_theme_on
1319 customlist
1320 theme_source
1321 builtin_name
1322
1323 Methods:
1324 deactivate_current_config
1325 save_all_changed_extensions
1326 activate_config_changes
1327 set_theme_type
1328 """
1329 theme_name = self.custom_name.get()
1330 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001331 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001332 'Delete Theme', delmsg % theme_name, parent=self):
1333 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001334 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001335 # Remove theme from changes, config, and file.
1336 changes.delete_section('highlight', theme_name)
1337 # Reload user theme list.
1338 item_list = idleConf.GetSectionList('user', 'highlight')
1339 item_list.sort()
1340 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001341 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001342 self.customlist.SetMenu(item_list, '- no custom themes -')
1343 else:
1344 self.customlist.SetMenu(item_list, item_list[0])
1345 # Revert to default theme.
1346 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1347 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1348 # User can't back out of these changes, they must be applied now.
1349 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001350 self.cd.save_all_changed_extensions()
1351 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001352 self.set_theme_type()
1353
1354
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001355class KeysPage(Frame):
1356
1357 def __init__(self, master):
1358 super().__init__(master)
Mark Rosemanc579ad12020-10-24 16:45:00 -07001359 self.cd = master.winfo_toplevel()
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001360 self.create_page_keys()
1361 self.load_key_cfg()
1362
1363 def create_page_keys(self):
1364 """Return frame of widgets for Keys tab.
1365
1366 Enable users to provisionally change both individual and sets of
1367 keybindings (shortcut keys). Except for features implemented as
1368 extensions, keybindings are stored in complete sets called
1369 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1370 as far as the dialog is concerned. Any keyset can be used as the
1371 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1372
1373 Function load_key_cfg() initializes tk variables and keyset
1374 lists and calls load_keys_list for the current keyset.
1375 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1376 keyset_source, which controls if the current set of keybindings
1377 are from a builtin or custom keyset. DynOptionMenus builtinlist
1378 and customlist contain lists of the builtin and custom keysets,
1379 respectively, and the current item from each list is stored in
1380 vars builtin_name and custom_name.
1381
1382 Button delete_custom_keys invokes delete_custom_keys() to delete
1383 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1384 save_custom_keys invokes save_as_new_key_set() which calls
1385 get_new_keys_name() and create_new_key_set() to save a custom keyset
1386 and its keybindings to idleConf.userCfg['keys'].
1387
1388 Listbox bindingslist contains all of the keybindings for the
1389 selected keyset. The keybindings are loaded in load_keys_list()
1390 and are pairs of (event, [keys]) where keys can be a list
1391 of one or more key combinations to bind to the same event.
1392 Mouse button 1 click invokes on_bindingslist_select(), which
1393 allows button_new_keys to be clicked.
1394
1395 So, an item is selected in listbindings, which activates
1396 button_new_keys, and clicking button_new_keys calls function
1397 get_new_keys(). Function get_new_keys() gets the key mappings from the
1398 current keyset for the binding event item that was selected. The
1399 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001400 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001401 to be entered for that binding event. If the keys aren't
1402 changed, nothing happens. If the keys are changed and the keyset
1403 is a builtin, function get_new_keys_name() will be called
1404 for input of a custom keyset name. If no name is given, then the
1405 change to the keybinding will abort and no updates will be made. If
1406 a custom name is entered in the prompt or if the current keyset was
1407 already custom (and thus didn't require a prompt), then
1408 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1409 with the change to the event binding. The item listing in bindingslist
1410 is updated with the new keys. Var keybinding is also set which invokes
1411 the callback function, var_changed_keybinding, to add the change to
1412 the 'keys' or 'extensions' changes tracker based on the binding type.
1413
1414 Tk Variables:
1415 keybinding: Action/key bindings.
1416
1417 Methods:
1418 load_keys_list: Reload active set.
1419 create_new_key_set: Combine active keyset and changes.
1420 set_keys_type: Command for keyset_source.
1421 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1422 deactivate_current_config: Remove keys bindings in editors.
1423
1424 Widgets for KeysPage(frame): (*) widgets bound to self
1425 frame_key_sets: LabelFrame
1426 frames[0]: Frame
1427 (*)builtin_keyset_on: Radiobutton - var keyset_source
1428 (*)custom_keyset_on: Radiobutton - var keyset_source
1429 (*)builtinlist: DynOptionMenu - var builtin_name,
1430 func keybinding_selected
1431 (*)customlist: DynOptionMenu - var custom_name,
1432 func keybinding_selected
1433 (*)keys_message: Label
1434 frames[1]: Frame
1435 (*)button_delete_custom_keys: Button - delete_custom_keys
1436 (*)button_save_custom_keys: Button - save_as_new_key_set
1437 frame_custom: LabelFrame
1438 frame_target: Frame
1439 target_title: Label
1440 scroll_target_y: Scrollbar
1441 scroll_target_x: Scrollbar
1442 (*)bindingslist: ListBox - on_bindingslist_select
1443 (*)button_new_keys: Button - get_new_keys & ..._name
1444 """
1445 self.builtin_name = tracers.add(
1446 StringVar(self), self.var_changed_builtin_name)
1447 self.custom_name = tracers.add(
1448 StringVar(self), self.var_changed_custom_name)
1449 self.keyset_source = tracers.add(
1450 BooleanVar(self), self.var_changed_keyset_source)
1451 self.keybinding = tracers.add(
1452 StringVar(self), self.var_changed_keybinding)
1453
1454 # Create widgets:
1455 # body and section frames.
1456 frame_custom = LabelFrame(
1457 self, borderwidth=2, relief=GROOVE,
1458 text=' Custom Key Bindings ')
1459 frame_key_sets = LabelFrame(
1460 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1461 # frame_custom.
1462 frame_target = Frame(frame_custom)
1463 target_title = Label(frame_target, text='Action - Key(s)')
1464 scroll_target_y = Scrollbar(frame_target)
1465 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1466 self.bindingslist = Listbox(
1467 frame_target, takefocus=FALSE, exportselection=FALSE)
1468 self.bindingslist.bind('<ButtonRelease-1>',
1469 self.on_bindingslist_select)
1470 scroll_target_y['command'] = self.bindingslist.yview
1471 scroll_target_x['command'] = self.bindingslist.xview
1472 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1473 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1474 self.button_new_keys = Button(
1475 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001476 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001477 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001478 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001479 for i in range(2)]
1480 self.builtin_keyset_on = Radiobutton(
1481 frames[0], variable=self.keyset_source, value=1,
1482 command=self.set_keys_type, text='Use a Built-in Key Set')
1483 self.custom_keyset_on = Radiobutton(
1484 frames[0], variable=self.keyset_source, value=0,
1485 command=self.set_keys_type, text='Use a Custom Key Set')
1486 self.builtinlist = DynOptionMenu(
1487 frames[0], self.builtin_name, None, command=None)
1488 self.customlist = DynOptionMenu(
1489 frames[0], self.custom_name, None, command=None)
1490 self.button_delete_custom_keys = Button(
1491 frames[1], text='Delete Custom Key Set',
1492 command=self.delete_custom_keys)
1493 self.button_save_custom_keys = Button(
1494 frames[1], text='Save as New Custom Key Set',
1495 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001496 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001497
1498 # Pack widgets:
1499 # body.
1500 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1501 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1502 # frame_custom.
1503 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1504 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1505 # frame_target.
1506 frame_target.columnconfigure(0, weight=1)
1507 frame_target.rowconfigure(1, weight=1)
1508 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1509 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1510 scroll_target_y.grid(row=1, column=1, sticky=NS)
1511 scroll_target_x.grid(row=2, column=0, sticky=EW)
1512 # frame_key_sets.
1513 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1514 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1515 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1516 self.customlist.grid(row=1, column=1, sticky=NSEW)
1517 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1518 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1519 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1520 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1521 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1522
1523 def load_key_cfg(self):
1524 "Load current configuration settings for the keybinding options."
1525 # Set current keys type radiobutton.
1526 self.keyset_source.set(idleConf.GetOption(
1527 'main', 'Keys', 'default', type='bool', default=1))
1528 # Set current keys.
1529 current_option = idleConf.CurrentKeys()
1530 # Load available keyset option menus.
1531 if self.keyset_source.get(): # Default theme selected.
1532 item_list = idleConf.GetSectionList('default', 'keys')
1533 item_list.sort()
1534 self.builtinlist.SetMenu(item_list, current_option)
1535 item_list = idleConf.GetSectionList('user', 'keys')
1536 item_list.sort()
1537 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001538 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001539 self.custom_name.set('- no custom keys -')
1540 else:
1541 self.customlist.SetMenu(item_list, item_list[0])
1542 else: # User key set selected.
1543 item_list = idleConf.GetSectionList('user', 'keys')
1544 item_list.sort()
1545 self.customlist.SetMenu(item_list, current_option)
1546 item_list = idleConf.GetSectionList('default', 'keys')
1547 item_list.sort()
1548 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1549 self.set_keys_type()
1550 # Load keyset element list.
1551 keyset_name = idleConf.CurrentKeys()
1552 self.load_keys_list(keyset_name)
1553
1554 def var_changed_builtin_name(self, *params):
1555 "Process selection of builtin key set."
1556 old_keys = (
1557 'IDLE Classic Windows',
1558 'IDLE Classic Unix',
1559 'IDLE Classic Mac',
1560 'IDLE Classic OSX',
1561 )
1562 value = self.builtin_name.get()
1563 if value not in old_keys:
1564 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1565 changes.add_option('main', 'Keys', 'name', old_keys[0])
1566 changes.add_option('main', 'Keys', 'name2', value)
1567 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001568 else:
1569 changes.add_option('main', 'Keys', 'name', value)
1570 changes.add_option('main', 'Keys', 'name2', '')
1571 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001572 self.load_keys_list(value)
1573
1574 def var_changed_custom_name(self, *params):
1575 "Process selection of custom key set."
1576 value = self.custom_name.get()
1577 if value != '- no custom keys -':
1578 changes.add_option('main', 'Keys', 'name', value)
1579 self.load_keys_list(value)
1580
1581 def var_changed_keyset_source(self, *params):
1582 "Process toggle between builtin key set and custom key set."
1583 value = self.keyset_source.get()
1584 changes.add_option('main', 'Keys', 'default', value)
1585 if value:
1586 self.var_changed_builtin_name()
1587 else:
1588 self.var_changed_custom_name()
1589
1590 def var_changed_keybinding(self, *params):
1591 "Store change to a keybinding."
1592 value = self.keybinding.get()
1593 key_set = self.custom_name.get()
1594 event = self.bindingslist.get(ANCHOR).split()[0]
1595 if idleConf.IsCoreBinding(event):
1596 changes.add_option('keys', key_set, event, value)
1597 else: # Event is an extension binding.
1598 ext_name = idleConf.GetExtnNameForEvent(event)
1599 ext_keybind_section = ext_name + '_cfgBindings'
1600 changes.add_option('extensions', ext_keybind_section, event, value)
1601
1602 def set_keys_type(self):
1603 "Set available screen options based on builtin or custom key set."
1604 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001605 self.builtinlist['state'] = 'normal'
1606 self.customlist['state'] = 'disabled'
1607 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001608 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001609 self.builtinlist['state'] = 'disabled'
1610 self.custom_keyset_on.state(('!disabled',))
1611 self.customlist['state'] = 'normal'
1612 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001613
1614 def get_new_keys(self):
1615 """Handle event to change key binding for selected line.
1616
1617 A selection of a key/binding in the list of current
1618 bindings pops up a dialog to enter a new binding. If
1619 the current key set is builtin and a binding has
1620 changed, then a name for a custom key set needs to be
1621 entered for the change to be applied.
1622 """
1623 list_index = self.bindingslist.index(ANCHOR)
1624 binding = self.bindingslist.get(list_index)
1625 bind_name = binding.split()[0]
1626 if self.keyset_source.get():
1627 current_key_set_name = self.builtin_name.get()
1628 else:
1629 current_key_set_name = self.custom_name.get()
1630 current_bindings = idleConf.GetCurrentKeySet()
1631 if current_key_set_name in changes['keys']: # unsaved changes
1632 key_set_changes = changes['keys'][current_key_set_name]
1633 for event in key_set_changes:
1634 current_bindings[event] = key_set_changes[event].split()
1635 current_key_sequences = list(current_bindings.values())
1636 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1637 current_key_sequences).result
1638 if new_keys:
1639 if self.keyset_source.get(): # Current key set is a built-in.
1640 message = ('Your changes will be saved as a new Custom Key Set.'
1641 ' Enter a name for your new Custom Key Set below.')
1642 new_keyset = self.get_new_keys_name(message)
1643 if not new_keyset: # User cancelled custom key set creation.
1644 self.bindingslist.select_set(list_index)
1645 self.bindingslist.select_anchor(list_index)
1646 return
1647 else: # Create new custom key set based on previously active key set.
1648 self.create_new_key_set(new_keyset)
1649 self.bindingslist.delete(list_index)
1650 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1651 self.bindingslist.select_set(list_index)
1652 self.bindingslist.select_anchor(list_index)
1653 self.keybinding.set(new_keys)
1654 else:
1655 self.bindingslist.select_set(list_index)
1656 self.bindingslist.select_anchor(list_index)
1657
1658 def get_new_keys_name(self, message):
1659 "Return new key set name from query popup."
1660 used_names = (idleConf.GetSectionList('user', 'keys') +
1661 idleConf.GetSectionList('default', 'keys'))
1662 new_keyset = SectionName(
1663 self, 'New Custom Key Set', message, used_names).result
1664 return new_keyset
1665
1666 def save_as_new_key_set(self):
1667 "Prompt for name of new key set and save changes using that name."
1668 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1669 if new_keys_name:
1670 self.create_new_key_set(new_keys_name)
1671
1672 def on_bindingslist_select(self, event):
1673 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001674 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001675
1676 def create_new_key_set(self, new_key_set_name):
1677 """Create a new custom key set with the given name.
1678
1679 Copy the bindings/keys from the previously active keyset
1680 to the new keyset and activate the new custom keyset.
1681 """
1682 if self.keyset_source.get():
1683 prev_key_set_name = self.builtin_name.get()
1684 else:
1685 prev_key_set_name = self.custom_name.get()
1686 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1687 new_keys = {}
1688 for event in prev_keys: # Add key set to changed items.
1689 event_name = event[2:-2] # Trim off the angle brackets.
1690 binding = ' '.join(prev_keys[event])
1691 new_keys[event_name] = binding
1692 # Handle any unsaved changes to prev key set.
1693 if prev_key_set_name in changes['keys']:
1694 key_set_changes = changes['keys'][prev_key_set_name]
1695 for event in key_set_changes:
1696 new_keys[event] = key_set_changes[event]
1697 # Save the new key set.
1698 self.save_new_key_set(new_key_set_name, new_keys)
1699 # Change GUI over to the new key set.
1700 custom_key_list = idleConf.GetSectionList('user', 'keys')
1701 custom_key_list.sort()
1702 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1703 self.keyset_source.set(0)
1704 self.set_keys_type()
1705
1706 def load_keys_list(self, keyset_name):
1707 """Reload the list of action/key binding pairs for the active key set.
1708
1709 An action/key binding can be selected to change the key binding.
1710 """
1711 reselect = False
1712 if self.bindingslist.curselection():
1713 reselect = True
1714 list_index = self.bindingslist.index(ANCHOR)
1715 keyset = idleConf.GetKeySet(keyset_name)
1716 bind_names = list(keyset.keys())
1717 bind_names.sort()
1718 self.bindingslist.delete(0, END)
1719 for bind_name in bind_names:
1720 key = ' '.join(keyset[bind_name])
1721 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1722 if keyset_name in changes['keys']:
1723 # Handle any unsaved changes to this key set.
1724 if bind_name in changes['keys'][keyset_name]:
1725 key = changes['keys'][keyset_name][bind_name]
1726 self.bindingslist.insert(END, bind_name+' - '+key)
1727 if reselect:
1728 self.bindingslist.see(list_index)
1729 self.bindingslist.select_set(list_index)
1730 self.bindingslist.select_anchor(list_index)
1731
1732 @staticmethod
1733 def save_new_key_set(keyset_name, keyset):
1734 """Save a newly created core key set.
1735
1736 Add keyset to idleConf.userCfg['keys'], not to disk.
1737 If the keyset doesn't exist, it is created. The
1738 binding/keys are taken from the keyset argument.
1739
1740 keyset_name - string, the name of the new key set
1741 keyset - dictionary containing the new keybindings
1742 """
Cheryl Sabelladd023ad2020-01-27 17:15:56 -05001743 idleConf.userCfg['keys'].AddSection(keyset_name)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001744 for event in keyset:
1745 value = keyset[event]
1746 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1747
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001748 def askyesno(self, *args, **kwargs):
1749 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001750 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001751
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001752 def delete_custom_keys(self):
1753 """Handle event to delete a custom key set.
1754
1755 Applying the delete deactivates the current configuration and
1756 reverts to the default. The custom key set is permanently
1757 deleted from the config file.
1758 """
1759 keyset_name = self.custom_name.get()
1760 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001761 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001762 'Delete Key Set', delmsg % keyset_name, parent=self):
1763 return
1764 self.cd.deactivate_current_config()
1765 # Remove key set from changes, config, and file.
1766 changes.delete_section('keys', keyset_name)
1767 # Reload user key set list.
1768 item_list = idleConf.GetSectionList('user', 'keys')
1769 item_list.sort()
1770 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001771 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001772 self.customlist.SetMenu(item_list, '- no custom keys -')
1773 else:
1774 self.customlist.SetMenu(item_list, item_list[0])
1775 # Revert to default key set.
1776 self.keyset_source.set(idleConf.defaultCfg['main']
Tal Einat604e7b92018-09-25 15:10:14 +03001777 .Get('Keys', 'default'))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001778 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
Tal Einat604e7b92018-09-25 15:10:14 +03001779 or idleConf.default_keys())
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001780 # User can't back out of these changes, they must be applied now.
1781 changes.save_all()
1782 self.cd.save_all_changed_extensions()
1783 self.cd.activate_config_changes()
1784 self.set_keys_type()
1785
1786
csabellae8eb17b2017-07-30 18:39:17 -04001787class GenPage(Frame):
1788
csabella6f446be2017-08-01 00:24:07 -04001789 def __init__(self, master):
1790 super().__init__(master)
Tal Einat1ebee372019-07-23 13:02:11 +03001791
1792 self.init_validators()
csabellae8eb17b2017-07-30 18:39:17 -04001793 self.create_page_general()
1794 self.load_general_cfg()
1795
Tal Einat1ebee372019-07-23 13:02:11 +03001796 def init_validators(self):
1797 digits_or_empty_re = re.compile(r'[0-9]*')
1798 def is_digits_or_empty(s):
1799 "Return 's is blank or contains only digits'"
1800 return digits_or_empty_re.fullmatch(s) is not None
1801 self.digits_only = (self.register(is_digits_or_empty), '%P',)
1802
csabellae8eb17b2017-07-30 18:39:17 -04001803 def create_page_general(self):
1804 """Return frame of widgets for General tab.
1805
1806 Enable users to provisionally change general options. Function
Terry Jan Reedy0acb6462019-07-30 18:14:58 -04001807 load_general_cfg initializes tk variables and helplist using
csabellae8eb17b2017-07-30 18:39:17 -04001808 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1809 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1810 set var autosave. Entry boxes win_width_int and win_height_int
1811 set var win_width and win_height. Setting var_name invokes the
1812 default callback that adds option to changes.
1813
1814 Helplist: load_general_cfg loads list user_helplist with
1815 name, position pairs and copies names to listbox helplist.
1816 Clicking a name invokes help_source selected. Clicking
1817 button_helplist_name invokes helplist_item_name, which also
1818 changes user_helplist. These functions all call
1819 set_add_delete_state. All but load call update_help_changes to
1820 rewrite changes['main']['HelpFiles'].
1821
Cheryl Sabella2f896462017-08-14 21:21:43 -04001822 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001823 frame_window: LabelFrame
1824 frame_run: Frame
1825 startup_title: Label
1826 (*)startup_editor_on: Radiobutton - startup_edit
1827 (*)startup_shell_on: Radiobutton - startup_edit
1828 frame_win_size: Frame
1829 win_size_title: Label
1830 win_width_title: Label
1831 (*)win_width_int: Entry - win_width
1832 win_height_title: Label
1833 (*)win_height_int: Entry - win_height
Zackery Spytz9c284492019-11-13 00:13:33 -07001834 frame_cursor_blink: Frame
1835 cursor_blink_title: Label
1836 (*)cursor_blink_bool: Checkbutton - cursor_blink
Cheryl Sabella845d8642018-02-04 18:15:21 -05001837 frame_autocomplete: Frame
1838 auto_wait_title: Label
1839 (*)auto_wait_int: Entry - autocomplete_wait
1840 frame_paren1: Frame
1841 paren_style_title: Label
1842 (*)paren_style_type: OptionMenu - paren_style
1843 frame_paren2: Frame
1844 paren_time_title: Label
1845 (*)paren_flash_time: Entry - flash_delay
1846 (*)bell_on: Checkbutton - paren_bell
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001847 frame_editor: LabelFrame
1848 frame_save: Frame
1849 run_save_title: Label
1850 (*)save_ask_on: Radiobutton - autosave
1851 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella845d8642018-02-04 18:15:21 -05001852 frame_format: Frame
1853 format_width_title: Label
1854 (*)format_width_int: Entry - format_width
Tal Einat7123ea02019-07-23 15:22:11 +03001855 frame_line_numbers_default: Frame
1856 line_numbers_default_title: Label
1857 (*)line_numbers_default_bool: Checkbutton - line_numbers_default
Cheryl Sabella845d8642018-02-04 18:15:21 -05001858 frame_context: Frame
1859 context_title: Label
1860 (*)context_int: Entry - context_lines
Tal Einat604e7b92018-09-25 15:10:14 +03001861 frame_shell: LabelFrame
1862 frame_auto_squeeze_min_lines: Frame
1863 auto_squeeze_min_lines_title: Label
1864 (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines
csabellae8eb17b2017-07-30 18:39:17 -04001865 """
wohlganger58fc71c2017-09-10 16:19:47 -05001866 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001867 self.startup_edit = tracers.add(
1868 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001869 self.win_width = tracers.add(
1870 StringVar(self), ('main', 'EditorWindow', 'width'))
1871 self.win_height = tracers.add(
1872 StringVar(self), ('main', 'EditorWindow', 'height'))
Zackery Spytz9c284492019-11-13 00:13:33 -07001873 self.cursor_blink = tracers.add(
1874 BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink'))
wohlganger58fc71c2017-09-10 16:19:47 -05001875 self.autocomplete_wait = tracers.add(
1876 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1877 self.paren_style = tracers.add(
1878 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1879 self.flash_delay = tracers.add(
1880 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1881 self.paren_bell = tracers.add(
1882 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001883
Tal Einat604e7b92018-09-25 15:10:14 +03001884 self.auto_squeeze_min_lines = tracers.add(
1885 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
1886
wohlganger58fc71c2017-09-10 16:19:47 -05001887 self.autosave = tracers.add(
1888 IntVar(self), ('main', 'General', 'autosave'))
1889 self.format_width = tracers.add(
1890 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
Tal Einat7123ea02019-07-23 15:22:11 +03001891 self.line_numbers_default = tracers.add(
1892 BooleanVar(self),
1893 ('main', 'EditorWindow', 'line-numbers-default'))
wohlganger58fc71c2017-09-10 16:19:47 -05001894 self.context_lines = tracers.add(
Cheryl Sabella29996a12018-06-01 19:23:00 -04001895 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
wohlganger58fc71c2017-09-10 16:19:47 -05001896
1897 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001898 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001899 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1900 text=' Window Preferences')
1901 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1902 text=' Editor Preferences')
Tal Einat604e7b92018-09-25 15:10:14 +03001903 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
1904 text=' Shell Preferences')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001905 # Frame_window.
1906 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001907 startup_title = Label(frame_run, text='At Startup')
1908 self.startup_editor_on = Radiobutton(
1909 frame_run, variable=self.startup_edit, value=1,
1910 text="Open Edit Window")
1911 self.startup_shell_on = Radiobutton(
1912 frame_run, variable=self.startup_edit, value=0,
1913 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001914
wohlganger58fc71c2017-09-10 16:19:47 -05001915 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001916 win_size_title = Label(
1917 frame_win_size, text='Initial Window Size (in characters)')
1918 win_width_title = Label(frame_win_size, text='Width')
1919 self.win_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001920 frame_win_size, textvariable=self.win_width, width=3,
1921 validatecommand=self.digits_only, validate='key',
1922 )
csabellae8eb17b2017-07-30 18:39:17 -04001923 win_height_title = Label(frame_win_size, text='Height')
1924 self.win_height_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001925 frame_win_size, textvariable=self.win_height, width=3,
1926 validatecommand=self.digits_only, validate='key',
1927 )
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001928
Zackery Spytz9c284492019-11-13 00:13:33 -07001929 frame_cursor_blink = Frame(frame_window, borderwidth=0)
1930 cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink')
1931 self.cursor_blink_bool = Checkbutton(frame_cursor_blink,
1932 variable=self.cursor_blink, width=1)
1933
wohlganger58fc71c2017-09-10 16:19:47 -05001934 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1935 auto_wait_title = Label(frame_autocomplete,
1936 text='Completions Popup Wait (milliseconds)')
1937 self.auto_wait_int = Entry(frame_autocomplete, width=6,
Tal Einat1ebee372019-07-23 13:02:11 +03001938 textvariable=self.autocomplete_wait,
1939 validatecommand=self.digits_only,
1940 validate='key',
1941 )
wohlganger58fc71c2017-09-10 16:19:47 -05001942
1943 frame_paren1 = Frame(frame_window, borderwidth=0)
1944 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1945 self.paren_style_type = OptionMenu(
1946 frame_paren1, self.paren_style, 'expression',
1947 "opener","parens","expression")
1948 frame_paren2 = Frame(frame_window, borderwidth=0)
1949 paren_time_title = Label(
1950 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1951 '(0 is until next input)')
1952 self.paren_flash_time = Entry(
1953 frame_paren2, textvariable=self.flash_delay, width=6)
1954 self.bell_on = Checkbutton(
1955 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1956
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001957 # Frame_editor.
1958 frame_save = Frame(frame_editor, borderwidth=0)
1959 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1960 self.save_ask_on = Radiobutton(
1961 frame_save, variable=self.autosave, value=0,
1962 text="Prompt to Save")
1963 self.save_auto_on = Radiobutton(
1964 frame_save, variable=self.autosave, value=1,
1965 text='No Prompt')
1966
wohlganger58fc71c2017-09-10 16:19:47 -05001967 frame_format = Frame(frame_editor, borderwidth=0)
1968 format_width_title = Label(frame_format,
1969 text='Format Paragraph Max Width')
1970 self.format_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001971 frame_format, textvariable=self.format_width, width=4,
1972 validatecommand=self.digits_only, validate='key',
1973 )
wohlganger58fc71c2017-09-10 16:19:47 -05001974
Tal Einat7123ea02019-07-23 15:22:11 +03001975 frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
1976 line_numbers_default_title = Label(
1977 frame_line_numbers_default, text='Show line numbers in new windows')
1978 self.line_numbers_default_bool = Checkbutton(
1979 frame_line_numbers_default,
1980 variable=self.line_numbers_default,
1981 width=1)
1982
wohlganger58fc71c2017-09-10 16:19:47 -05001983 frame_context = Frame(frame_editor, borderwidth=0)
Cheryl Sabella29996a12018-06-01 19:23:00 -04001984 context_title = Label(frame_context, text='Max Context Lines :')
wohlganger58fc71c2017-09-10 16:19:47 -05001985 self.context_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001986 frame_context, textvariable=self.context_lines, width=3,
1987 validatecommand=self.digits_only, validate='key',
1988 )
wohlganger58fc71c2017-09-10 16:19:47 -05001989
Tal Einat604e7b92018-09-25 15:10:14 +03001990 # Frame_shell.
1991 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
1992 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
1993 text='Auto-Squeeze Min. Lines:')
1994 self.auto_squeeze_min_lines_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001995 frame_auto_squeeze_min_lines, width=4,
1996 textvariable=self.auto_squeeze_min_lines,
1997 validatecommand=self.digits_only, validate='key',
1998 )
wohlganger58fc71c2017-09-10 16:19:47 -05001999
csabellae8eb17b2017-07-30 18:39:17 -04002000 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002001 # Body.
2002 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2003 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Tal Einat604e7b92018-09-25 15:10:14 +03002004 frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04002005 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002006 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002007 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2008 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2009 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04002010 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002011 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002012 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2013 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2014 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
2015 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2016 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
Zackery Spytz9c284492019-11-13 00:13:33 -07002017 # frame_cursor_blink.
2018 frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X)
2019 cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2020 self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002021 # frame_autocomplete.
2022 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
2023 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2024 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
2025 # frame_paren.
2026 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
2027 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2028 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
2029 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
2030 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
2031 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
2032 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
2033
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002034 # frame_save.
2035 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
2036 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2037 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2038 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002039 # frame_format.
2040 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
2041 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2042 self.format_width_int.pack(side=TOP, padx=10, pady=5)
Tal Einat7123ea02019-07-23 15:22:11 +03002043 # frame_line_numbers_default.
2044 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
2045 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2046 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002047 # frame_context.
2048 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
2049 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2050 self.context_int.pack(side=TOP, padx=5, pady=5)
2051
Tal Einat604e7b92018-09-25 15:10:14 +03002052 # frame_auto_squeeze_min_lines
2053 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
2054 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2055 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
2056
csabellae8eb17b2017-07-30 18:39:17 -04002057 def load_general_cfg(self):
2058 "Load current configuration settings for the general options."
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -07002059 self.load_windows_cfg()
2060 self.load_shelled_cfg()
2061
2062 def load_windows_cfg(self):
wohlganger58fc71c2017-09-10 16:19:47 -05002063 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04002064 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05002065 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04002066 self.win_width.set(idleConf.GetOption(
2067 'main', 'EditorWindow', 'width', type='int'))
2068 self.win_height.set(idleConf.GetOption(
2069 'main', 'EditorWindow', 'height', type='int'))
Zackery Spytz9c284492019-11-13 00:13:33 -07002070 self.cursor_blink.set(idleConf.GetOption(
2071 'main', 'EditorWindow', 'cursor-blink', type='bool'))
wohlganger58fc71c2017-09-10 16:19:47 -05002072 self.autocomplete_wait.set(idleConf.GetOption(
2073 'extensions', 'AutoComplete', 'popupwait', type='int'))
2074 self.paren_style.set(idleConf.GetOption(
2075 'extensions', 'ParenMatch', 'style'))
2076 self.flash_delay.set(idleConf.GetOption(
2077 'extensions', 'ParenMatch', 'flash-delay', type='int'))
2078 self.paren_bell.set(idleConf.GetOption(
2079 'extensions', 'ParenMatch', 'bell'))
2080
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -07002081 def load_shelled_cfg(self):
wohlganger58fc71c2017-09-10 16:19:47 -05002082 # Set variables for editor windows.
2083 self.autosave.set(idleConf.GetOption(
2084 'main', 'General', 'autosave', default=0, type='bool'))
2085 self.format_width.set(idleConf.GetOption(
2086 'extensions', 'FormatParagraph', 'max-width', type='int'))
Tal Einat7123ea02019-07-23 15:22:11 +03002087 self.line_numbers_default.set(idleConf.GetOption(
2088 'main', 'EditorWindow', 'line-numbers-default', type='bool'))
wohlganger58fc71c2017-09-10 16:19:47 -05002089 self.context_lines.set(idleConf.GetOption(
Cheryl Sabella29996a12018-06-01 19:23:00 -04002090 'extensions', 'CodeContext', 'maxlines', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002091
Tal Einat604e7b92018-09-25 15:10:14 +03002092 # Set variables for shell windows.
2093 self.auto_squeeze_min_lines.set(idleConf.GetOption(
2094 'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
2095
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -07002096
2097class HelpFrame(LabelFrame):
2098
2099 def __init__(self, master, **cfg):
2100 super().__init__(master, **cfg)
2101 self.create_frame_help()
2102 self.load_helplist()
2103
2104 def create_frame_help(self):
2105 """Create LabelFrame for additional help menu sources.
2106
2107 load_helplist loads list user_helplist with
2108 name, position pairs and copies names to listbox helplist.
2109 Clicking a name invokes help_source selected. Clicking
2110 button_helplist_name invokes helplist_item_name, which also
2111 changes user_helplist. These functions all call
2112 set_add_delete_state. All but load call update_help_changes to
2113 rewrite changes['main']['HelpFiles'].
2114
2115 Widgets for HelpFrame(LabelFrame): (*) widgets bound to self
2116 frame_helplist: Frame
2117 (*)helplist: ListBox
2118 scroll_helplist: Scrollbar
2119 frame_buttons: Frame
2120 (*)button_helplist_edit
2121 (*)button_helplist_add
2122 (*)button_helplist_remove
2123 """
2124 # self = frame_help in dialog (until ExtPage class).
2125 frame_helplist = Frame(self)
2126 self.helplist = Listbox(
2127 frame_helplist, height=5, takefocus=True,
2128 exportselection=FALSE)
2129 scroll_helplist = Scrollbar(frame_helplist)
2130 scroll_helplist['command'] = self.helplist.yview
2131 self.helplist['yscrollcommand'] = scroll_helplist.set
2132 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
2133
2134 frame_buttons = Frame(self)
2135 self.button_helplist_edit = Button(
2136 frame_buttons, text='Edit', state='disabled',
2137 width=8, command=self.helplist_item_edit)
2138 self.button_helplist_add = Button(
2139 frame_buttons, text='Add',
2140 width=8, command=self.helplist_item_add)
2141 self.button_helplist_remove = Button(
2142 frame_buttons, text='Remove', state='disabled',
2143 width=8, command=self.helplist_item_remove)
2144
2145 # Pack frame_help.
2146 frame_helplist.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
2147 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
2148 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
2149 frame_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
2150 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
2151 self.button_helplist_add.pack(side=TOP, anchor=W)
2152 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04002153
2154 def help_source_selected(self, event):
2155 "Handle event for selecting additional help."
2156 self.set_add_delete_state()
2157
2158 def set_add_delete_state(self):
2159 "Toggle the state for the help list buttons based on list entries."
2160 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002161 self.button_helplist_edit.state(('disabled',))
2162 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002163 else: # Some entries.
2164 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002165 self.button_helplist_edit.state(('!disabled',))
2166 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002167 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002168 self.button_helplist_edit.state(('disabled',))
2169 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002170
2171 def helplist_item_add(self):
2172 """Handle add button for the help list.
2173
2174 Query for name and location of new help sources and add
2175 them to the list.
2176 """
2177 help_source = HelpSource(self, 'New Help Source').result
2178 if help_source:
2179 self.user_helplist.append(help_source)
2180 self.helplist.insert(END, help_source[0])
2181 self.update_help_changes()
2182
2183 def helplist_item_edit(self):
2184 """Handle edit button for the help list.
2185
2186 Query with existing help source information and update
2187 config if the values are changed.
2188 """
2189 item_index = self.helplist.index(ANCHOR)
2190 help_source = self.user_helplist[item_index]
2191 new_help_source = HelpSource(
2192 self, 'Edit Help Source',
2193 menuitem=help_source[0],
2194 filepath=help_source[1],
2195 ).result
2196 if new_help_source and new_help_source != help_source:
2197 self.user_helplist[item_index] = new_help_source
2198 self.helplist.delete(item_index)
2199 self.helplist.insert(item_index, new_help_source[0])
2200 self.update_help_changes()
2201 self.set_add_delete_state() # Selected will be un-selected
2202
2203 def helplist_item_remove(self):
2204 """Handle remove button for the help list.
2205
2206 Delete the help list item from config.
2207 """
2208 item_index = self.helplist.index(ANCHOR)
2209 del(self.user_helplist[item_index])
2210 self.helplist.delete(item_index)
2211 self.update_help_changes()
2212 self.set_add_delete_state()
2213
2214 def update_help_changes(self):
2215 "Clear and rebuild the HelpFiles section in changes"
2216 changes['main']['HelpFiles'] = {}
2217 for num in range(1, len(self.user_helplist) + 1):
2218 changes.add_option(
2219 'main', 'HelpFiles', str(num),
2220 ';'.join(self.user_helplist[num-1][:2]))
2221
Miss Islington (bot)2cfe0e72021-06-08 13:01:23 -07002222 def load_helplist(self):
2223 # Set additional help sources.
2224 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2225 self.helplist.delete(0, 'end')
2226 for help_item in self.user_helplist:
2227 self.helplist.insert(END, help_item[0])
2228 self.set_add_delete_state()
2229
csabellae8eb17b2017-07-30 18:39:17 -04002230
csabella45bf7232017-07-26 19:09:58 -04002231class VarTrace:
2232 """Maintain Tk variables trace state."""
2233
2234 def __init__(self):
2235 """Store Tk variables and callbacks.
2236
2237 untraced: List of tuples (var, callback)
2238 that do not have the callback attached
2239 to the Tk var.
2240 traced: List of tuples (var, callback) where
2241 that callback has been attached to the var.
2242 """
2243 self.untraced = []
2244 self.traced = []
2245
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002246 def clear(self):
2247 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002248 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002249 self.untraced.clear()
2250 self.traced.clear()
2251
csabella45bf7232017-07-26 19:09:58 -04002252 def add(self, var, callback):
2253 """Add (var, callback) tuple to untraced list.
2254
2255 Args:
2256 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002257 callback: Either function name to be used as a callback
2258 or a tuple with IdleConf config-type, section, and
2259 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002260
2261 Return:
2262 Tk variable instance.
2263 """
2264 if isinstance(callback, tuple):
2265 callback = self.make_callback(var, callback)
2266 self.untraced.append((var, callback))
2267 return var
2268
2269 @staticmethod
2270 def make_callback(var, config):
2271 "Return default callback function to add values to changes instance."
2272 def default_callback(*params):
2273 "Add config values to changes instance."
2274 changes.add_option(*config, var.get())
2275 return default_callback
2276
2277 def attach(self):
2278 "Attach callback to all vars that are not traced."
2279 while self.untraced:
2280 var, callback = self.untraced.pop()
2281 var.trace_add('write', callback)
2282 self.traced.append((var, callback))
2283
2284 def detach(self):
2285 "Remove callback from traced vars."
2286 while self.traced:
2287 var, callback = self.traced.pop()
2288 var.trace_remove('write', var.trace_info()[0][1])
2289 self.untraced.append((var, callback))
2290
2291
csabella5b591542017-07-28 14:40:59 -04002292tracers = VarTrace()
2293
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002294help_common = '''\
2295When you click either the Apply or Ok buttons, settings in this
2296dialog that are different from IDLE's default are saved in
2297a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002298these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002299machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002300'''
2301help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002302 'Fonts/Tabs':'''
2303Font sample: This shows what a selection of Basic Multilingual Plane
2304unicode characters look like for the current font selection. If the
2305selected font does not define a character, Tk attempts to find another
2306font that does. Substitute glyphs depend on what is available on a
2307particular system and will not necessarily have the same size as the
2308font selected. Line contains 20 characters up to Devanagari, 14 for
2309Tamil, and 10 for East Asia.
2310
2311Hebrew and Arabic letters should display right to left, starting with
2312alef, \u05d0 and \u0627. Arabic digits display left to right. The
2313Devanagari and Tamil lines start with digits. The East Asian lines
2314are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2315Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002316
2317You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002318''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002319 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002320Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002321The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002322be used with older IDLE releases if it is saved as a custom
2323theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002324''',
2325 'Keys': '''
2326Keys:
2327The IDLE Modern Unix key set is new in June 2016. It can only
2328be used with older IDLE releases if it is saved as a custom
2329key set, with a different name.
2330''',
wohlganger58fc71c2017-09-10 16:19:47 -05002331 'General': '''
2332General:
wohlgangerfae2c352017-06-27 21:36:23 -05002333
penguindustin96466302019-05-06 14:57:17 -04002334AutoComplete: Popupwait is milliseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002335cursor movement, before popping up completion box. Key char is '.' after
2336identifier or a '/' (or '\\' on Windows) within a string.
2337
2338FormatParagraph: Max-width is max chars in lines after re-formatting.
2339Use with paragraphs in both strings and comment blocks.
2340
2341ParenMatch: Style indicates what is highlighted when closer is entered:
2342'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2343'expression' (default) - also everything in between. Flash-delay is how
2344long to highlight if cursor is not moved (0 means forever).
Cheryl Sabella29996a12018-06-01 19:23:00 -04002345
2346CodeContext: Maxlines is the maximum number of code context lines to
2347display when Code Context is turned on for an editor window.
Tal Einat604e7b92018-09-25 15:10:14 +03002348
2349Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
2350of output to automatically "squeeze".
Cheryl Sabellae40e2a22021-01-05 02:26:43 -05002351''',
2352 'Extensions': '''
2353ZzDummy: This extension is provided as an example for how to create and
2354use an extension. Enable indicates whether the extension is active or
2355not; likewise enable_editor and enable_shell indicate which windows it
2356will be active on. For this extension, z-text is the text that will be
2357inserted at or removed from the beginning of the lines of selected text,
2358or the current line if no selection.
2359''',
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002360}
2361
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002362
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002363def is_int(s):
2364 "Return 's is blank or represents an int'"
2365 if not s:
2366 return True
2367 try:
2368 int(s)
2369 return True
2370 except ValueError:
2371 return False
2372
2373
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002374class VerticalScrolledFrame(Frame):
2375 """A pure Tkinter vertically scrollable frame.
2376
2377 * Use the 'interior' attribute to place widgets inside the scrollable frame
2378 * Construct and pack/place/grid normally
2379 * This frame only allows vertical scrolling
2380 """
2381 def __init__(self, parent, *args, **kw):
2382 Frame.__init__(self, parent, *args, **kw)
2383
csabella7eb58832017-07-04 21:30:58 -04002384 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002385 vscrollbar = Scrollbar(self, orient=VERTICAL)
2386 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002387 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002388 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002389 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2390 vscrollbar.config(command=canvas.yview)
2391
csabella7eb58832017-07-04 21:30:58 -04002392 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002393 canvas.xview_moveto(0)
2394 canvas.yview_moveto(0)
2395
csabella7eb58832017-07-04 21:30:58 -04002396 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002397 self.interior = interior = Frame(canvas)
2398 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2399
csabella7eb58832017-07-04 21:30:58 -04002400 # Track changes to the canvas and frame width and sync them,
2401 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002402 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002403 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002404 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2405 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002406 interior.bind('<Configure>', _configure_interior)
2407
2408 def _configure_canvas(event):
2409 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002410 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002411 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2412 canvas.bind('<Configure>', _configure_canvas)
2413
2414 return
2415
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002416
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002417if __name__ == '__main__':
Terry Jan Reedy4d921582018-06-19 19:12:52 -04002418 from unittest import main
2419 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
2420
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002421 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002422 run(ConfigDialog)