blob: 217f8fd0a5fb3560f537e22ada8fae30965bfba2 [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
Cheryl Sabella7028e592017-08-26 14:26:02 -040014from tkinter import (Toplevel, Listbox, Text, 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)
Georg Brandl14fc4272008-05-17 18:39:55 +000021import tkinter.colorchooser as tkColorChooser
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
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -040036
terryjreedy349abd92017-07-07 16:00:57 -040037changes = ConfigChanges()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040038# Reload changed options in the following classes.
Tal Einat604e7b92018-09-25 15:10:14 +030039reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
40 Squeezer)
terryjreedy349abd92017-07-07 16:00:57 -040041
csabella5b591542017-07-28 14:40:59 -040042
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000043class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -040044 """Config dialog for IDLE.
45 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000046
Terry Jan Reedybfebfd82017-09-30 17:37:53 -040047 def __init__(self, parent, title='', *, _htest=False, _utest=False):
csabella7eb58832017-07-04 21:30:58 -040048 """Show the tabbed dialog for user configuration.
49
csabella36329a42017-07-13 23:32:01 -040050 Args:
51 parent - parent of this dialog
52 title - string which is the title of this popup dialog
53 _htest - bool, change box location when running htest
54 _utest - bool, don't wait_window when running unittest
55
56 Note: Focus set on font page fontlist.
57
58 Methods:
59 create_widgets
60 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040061 """
Steven M. Gavad721c482001-07-31 10:46:53 +000062 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040063 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040064 if _htest:
65 parent.instance_dict = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080066 if not _utest:
67 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000068
Steven M. Gavad721c482001-07-31 10:46:53 +000069 self.configure(borderwidth=5)
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:
csabellae8eb17b2017-07-30 18:39:17 -040099 note: Notebook
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400100 highpage: HighPage
csabellae8eb17b2017-07-30 18:39:17 -0400101 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400102 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -0400103 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400104 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -0400105
106 Methods:
csabella36329a42017-07-13 23:32:01 -0400107 create_action_buttons
108 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400109 activate_config_changes: Tell editors to reload.
110 """
Terry Jan Reedyd6e2f262017-09-19 19:01:45 -0400111 self.note = note = Notebook(self)
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400112 self.highpage = HighPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400113 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400114 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400115 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400116 self.extpage = self.create_page_extensions()
117 note.add(self.fontpage, text='Fonts/Tabs')
118 note.add(self.highpage, text='Highlights')
119 note.add(self.keyspage, text=' Keys ')
120 note.add(self.genpage, text=' General ')
121 note.add(self.extpage, text='Extensions')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400122 note.enable_traversal()
123 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400124 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400125
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400126 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -0400127 """Return frame of action buttons for dialog.
128
129 Methods:
130 ok
131 apply
132 cancel
133 help
134
135 Widget Structure:
136 outer: Frame
137 buttons: Frame
138 (no assignment): Button (ok)
139 (no assignment): Button (apply)
140 (no assignment): Button (cancel)
141 (no assignment): Button (help)
142 (no assignment): Frame
143 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400144 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400145 # Changing the default padding on OSX results in unreadable
csabella7eb58832017-07-04 21:30:58 -0400146 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400147 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000148 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400149 padding_args = {'padding': (6, 3)}
150 outer = Frame(self, padding=2)
151 buttons = Frame(outer, padding=2)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400152 for txt, cmd in (
csabellabac7d332017-06-26 17:46:26 -0400153 ('Ok', self.ok),
154 ('Apply', self.apply),
155 ('Cancel', self.cancel),
156 ('Help', self.help)):
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400157 Button(buttons, text=txt, command=cmd, takefocus=FALSE,
csabellabac7d332017-06-26 17:46:26 -0400158 **padding_args).pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -0400159 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400160 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
161 buttons.pack(side=BOTTOM)
162 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400163
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400164 def ok(self):
165 """Apply config changes, then dismiss dialog.
166
167 Methods:
168 apply
169 destroy: inherited
170 """
171 self.apply()
172 self.destroy()
173
174 def apply(self):
175 """Apply config changes and leave dialog open.
176
177 Methods:
178 deactivate_current_config
179 save_all_changed_extensions
180 activate_config_changes
181 """
182 self.deactivate_current_config()
183 changes.save_all()
184 self.save_all_changed_extensions()
185 self.activate_config_changes()
186
187 def cancel(self):
188 """Dismiss config dialog.
189
190 Methods:
191 destroy: inherited
192 """
193 self.destroy()
194
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300195 def destroy(self):
196 global font_sample_text
197 font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
Tal Einat10ea9402018-08-02 09:18:29 +0300198 self.grab_release()
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300199 super().destroy()
200
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400201 def help(self):
202 """Create textview for config dialog help.
203
Xtreakd9677f32019-06-03 09:51:15 +0530204 Attributes accessed:
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400205 note
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400206
207 Methods:
208 view_text: Method from textview module.
209 """
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400210 page = self.note.tab(self.note.select(), option='text').strip()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400211 view_text(self, title='Help for IDLE preferences',
212 text=help_common+help_pages.get(page, ''))
213
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400214 def deactivate_current_config(self):
215 """Remove current key bindings.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400216 Iterate over window instances defined in parent and remove
217 the keybindings.
218 """
219 # Before a config is saved, some cleanup of current
220 # config must be done - remove the previous keybindings.
221 win_instances = self.parent.instance_dict.keys()
222 for instance in win_instances:
223 instance.RemoveKeybindings()
224
225 def activate_config_changes(self):
226 """Apply configuration changes to current windows.
227
228 Dynamically update the current parent window instances
229 with some of the configuration changes.
230 """
231 win_instances = self.parent.instance_dict.keys()
232 for instance in win_instances:
233 instance.ResetColorizer()
234 instance.ResetFont()
235 instance.set_notabs_indentwidth()
236 instance.ApplyKeybindings()
237 instance.reset_help_menu_entries()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -0400238 for klass in reloadables:
239 klass.reload()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400240
csabellabac7d332017-06-26 17:46:26 -0400241 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400242 """Part of the config dialog used for configuring IDLE extensions.
243
244 This code is generic - it works for any and all IDLE extensions.
245
246 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400247 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400248 GUI interface to change the configuration values, and saves the
249 changes using idleConf.
250
251 Not all changes take effect immediately - some may require restarting IDLE.
252 This depends on each extension's implementation.
253
254 All values are treated as text, and it is up to the user to supply
255 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300256 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -0400257
258 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300259 load_extensions:
csabella36329a42017-07-13 23:32:01 -0400260 extension_selected: Handle selection from list.
261 create_extension_frame: Hold widgets for one extension.
262 set_extension_value: Set in userCfg['extensions'].
263 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400264 """
265 parent = self.parent
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400266 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400267 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
268 self.ext_userCfg = idleConf.userCfg['extensions']
269 self.is_int = self.register(is_int)
270 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -0400271 # Create widgets - a listbox shows all available extensions, with the
272 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400273 self.extension_names = StringVar(self)
274 frame.rowconfigure(0, weight=1)
275 frame.columnconfigure(2, weight=1)
276 self.extension_list = Listbox(frame, listvariable=self.extension_names,
277 selectmode='browse')
278 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
279 scroll = Scrollbar(frame, command=self.extension_list.yview)
280 self.extension_list.yscrollcommand=scroll.set
281 self.details_frame = LabelFrame(frame, width=250, height=250)
282 self.extension_list.grid(column=0, row=0, sticky='nws')
283 scroll.grid(column=1, row=0, sticky='ns')
284 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
Cheryl Sabella7028e592017-08-26 14:26:02 -0400285 frame.configure(padding=10)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400286 self.config_frame = {}
287 self.current_extension = None
288
289 self.outerframe = self # TEMPORARY
290 self.tabbed_page_set = self.extension_list # TEMPORARY
291
csabella7eb58832017-07-04 21:30:58 -0400292 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400293 ext_names = ''
294 for ext_name in sorted(self.extensions):
295 self.create_extension_frame(ext_name)
296 ext_names = ext_names + '{' + ext_name + '} '
297 self.extension_names.set(ext_names)
298 self.extension_list.selection_set(0)
299 self.extension_selected(None)
300
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400301 return frame
302
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400303 def load_extensions(self):
304 "Fill self.extensions with data from the default and user configs."
305 self.extensions = {}
306 for ext_name in idleConf.GetExtensions(active_only=False):
wohlganger58fc71c2017-09-10 16:19:47 -0500307 # Former built-in extensions are already filtered out.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400308 self.extensions[ext_name] = []
309
310 for ext_name in self.extensions:
311 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
312
csabella7eb58832017-07-04 21:30:58 -0400313 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400314 enables = [opt_name for opt_name in opt_list
315 if opt_name.startswith('enable')]
316 for opt_name in enables:
317 opt_list.remove(opt_name)
318 opt_list = enables + opt_list
319
320 for opt_name in opt_list:
321 def_str = self.ext_defaultCfg.Get(
322 ext_name, opt_name, raw=True)
323 try:
324 def_obj = {'True':True, 'False':False}[def_str]
325 opt_type = 'bool'
326 except KeyError:
327 try:
328 def_obj = int(def_str)
329 opt_type = 'int'
330 except ValueError:
331 def_obj = def_str
332 opt_type = None
333 try:
334 value = self.ext_userCfg.Get(
335 ext_name, opt_name, type=opt_type, raw=True,
336 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -0400337 except ValueError: # Need this until .Get fixed.
338 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400339 var = StringVar(self)
340 var.set(str(value))
341
342 self.extensions[ext_name].append({'name': opt_name,
343 'type': opt_type,
344 'default': def_str,
345 'value': value,
346 'var': var,
347 })
348
349 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -0400350 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400351 newsel = self.extension_list.curselection()
352 if newsel:
353 newsel = self.extension_list.get(newsel)
354 if newsel is None or newsel != self.current_extension:
355 if self.current_extension:
356 self.details_frame.config(text='')
357 self.config_frame[self.current_extension].grid_forget()
358 self.current_extension = None
359 if newsel:
360 self.details_frame.config(text=newsel)
361 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
362 self.current_extension = newsel
363
364 def create_extension_frame(self, ext_name):
365 """Create a frame holding the widgets to configure one extension"""
366 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
367 self.config_frame[ext_name] = f
368 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -0400369 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400370 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -0400371 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400372 label = Label(entry_area, text=opt['name'])
373 label.grid(row=row, column=0, sticky=NW)
374 var = opt['var']
375 if opt['type'] == 'bool':
Cheryl Sabella7028e592017-08-26 14:26:02 -0400376 Checkbutton(entry_area, variable=var,
377 onvalue='True', offvalue='False', width=8
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400378 ).grid(row=row, column=1, sticky=W, padx=7)
379 elif opt['type'] == 'int':
380 Entry(entry_area, textvariable=var, validate='key',
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400381 validatecommand=(self.is_int, '%P'), width=10
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400382 ).grid(row=row, column=1, sticky=NSEW, padx=7)
383
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400384 else: # type == 'str'
385 # Limit size to fit non-expanding space with larger font.
386 Entry(entry_area, textvariable=var, width=15
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400387 ).grid(row=row, column=1, sticky=NSEW, padx=7)
388 return
389
390 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400391 """Return True if the configuration was added or changed.
392
393 If the value is the same as the default, then remove it
394 from user config file.
395 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400396 name = opt['name']
397 default = opt['default']
398 value = opt['var'].get().strip() or default
399 opt['var'].set(value)
400 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400401 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400402 if (value == default):
403 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400404 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400405 return self.ext_userCfg.SetOption(section, name, value)
406
407 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400408 """Save configuration changes to the user config file.
409
410 Attributes accessed:
411 extensions
412
413 Methods:
414 set_extension_value
415 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400416 has_changes = False
417 for ext_name in self.extensions:
418 options = self.extensions[ext_name]
419 for opt in options:
420 if self.set_extension_value(ext_name, opt):
421 has_changes = True
422 if has_changes:
423 self.ext_userCfg.Save()
424
425
csabella6f446be2017-08-01 00:24:07 -0400426# class TabPage(Frame): # A template for Page classes.
427# def __init__(self, master):
428# super().__init__(master)
429# self.create_page_tab()
430# self.load_tab_cfg()
431# def create_page_tab(self):
432# # Define tk vars and register var and callback with tracers.
433# # Create subframes and widgets.
434# # Pack widgets.
435# def load_tab_cfg(self):
436# # Initialize widgets with data from idleConf.
437# def var_changed_var_name():
438# # For each tk var that needs other than default callback.
439# def other_methods():
440# # Define tab-specific behavior.
441
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300442font_sample_text = (
443 '<ASCII/Latin1>\n'
444 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
445 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
446 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
447 '\n<IPA,Greek,Cyrillic>\n'
448 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
449 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
450 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
451 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
452 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
453 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
454 '\n<Hebrew, Arabic>\n'
455 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
456 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
457 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
458 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
459 '\n<Devanagari, Tamil>\n'
460 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
461 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
462 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
463 '\u0b85\u0b87\u0b89\u0b8e\n'
464 '\n<East Asian>\n'
465 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
466 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
467 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
468 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
469 )
470
csabella6f446be2017-08-01 00:24:07 -0400471
csabella9397e2a2017-07-30 13:34:25 -0400472class FontPage(Frame):
473
csabella6f446be2017-08-01 00:24:07 -0400474 def __init__(self, master, highpage):
475 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400476 self.highlight_sample = highpage.highlight_sample
477 self.create_page_font_tab()
478 self.load_font_cfg()
479 self.load_tab_cfg()
480
481 def create_page_font_tab(self):
482 """Return frame of widgets for Font/Tabs tab.
483
484 Fonts: Enable users to provisionally change font face, size, or
485 boldness and to see the consequence of proposed choices. Each
486 action set 3 options in changes structuree and changes the
487 corresponding aspect of the font sample on this page and
488 highlight sample on highlight page.
489
490 Function load_font_cfg initializes font vars and widgets from
491 idleConf entries and tk.
492
493 Fontlist: mouse button 1 click or up or down key invoke
494 on_fontlist_select(), which sets var font_name.
495
496 Sizelist: clicking the menubutton opens the dropdown menu. A
497 mouse button 1 click or return key sets var font_size.
498
499 Bold_toggle: clicking the box toggles var font_bold.
500
501 Changing any of the font vars invokes var_changed_font, which
502 adds all 3 font options to changes and calls set_samples.
503 Set_samples applies a new font constructed from the font vars to
Leo Ariasc3d95082018-02-03 18:36:10 -0600504 font_sample and to highlight_sample on the highlight page.
csabella9397e2a2017-07-30 13:34:25 -0400505
506 Tabs: Enable users to change spaces entered for indent tabs.
507 Changing indent_scale value with the mouse sets Var space_num,
508 which invokes the default callback to add an entry to
509 changes. Load_tab_cfg initializes space_num to default.
510
Cheryl Sabella2f896462017-08-14 21:21:43 -0400511 Widgets for FontPage(Frame): (*) widgets bound to self
512 frame_font: LabelFrame
513 frame_font_name: Frame
514 font_name_title: Label
515 (*)fontlist: ListBox - font_name
516 scroll_font: Scrollbar
517 frame_font_param: Frame
518 font_size_title: Label
519 (*)sizelist: DynOptionMenu - font_size
520 (*)bold_toggle: Checkbutton - font_bold
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400521 frame_sample: LabelFrame
522 (*)font_sample: Label
Cheryl Sabella2f896462017-08-14 21:21:43 -0400523 frame_indent: LabelFrame
524 indent_title: Label
525 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400526 """
csabella6f446be2017-08-01 00:24:07 -0400527 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
528 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
529 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400530 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
531
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400532 # Define frames and widgets.
csabella9397e2a2017-07-30 13:34:25 -0400533 frame_font = LabelFrame(
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400534 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
535 frame_sample = LabelFrame(
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300536 self, borderwidth=2, relief=GROOVE,
537 text=' Font Sample (Editable) ')
csabella9397e2a2017-07-30 13:34:25 -0400538 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400539 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400540 # frame_font.
541 frame_font_name = Frame(frame_font)
542 frame_font_param = Frame(frame_font)
543 font_name_title = Label(
544 frame_font_name, justify=LEFT, text='Font Face :')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400545 self.fontlist = Listbox(frame_font_name, height=15,
csabella9397e2a2017-07-30 13:34:25 -0400546 takefocus=True, exportselection=FALSE)
547 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
548 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
549 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
550 scroll_font = Scrollbar(frame_font_name)
551 scroll_font.config(command=self.fontlist.yview)
552 self.fontlist.config(yscrollcommand=scroll_font.set)
553 font_size_title = Label(frame_font_param, text='Size :')
554 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
555 self.bold_toggle = Checkbutton(
556 frame_font_param, variable=self.font_bold,
557 onvalue=1, offvalue=0, text='Bold')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400558 # frame_sample.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300559 self.font_sample = Text(frame_sample, width=20, height=20)
560 self.font_sample.insert(END, font_sample_text)
csabella9397e2a2017-07-30 13:34:25 -0400561 # frame_indent.
562 indent_title = Label(
563 frame_indent, justify=LEFT,
564 text='Python Standard: 4 Spaces!')
565 self.indent_scale = Scale(
566 frame_indent, variable=self.space_num,
567 orient='horizontal', tickinterval=2, from_=2, to=16)
568
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400569 # Grid and pack widgets:
570 self.columnconfigure(1, weight=1)
571 frame_font.grid(row=0, column=0, padx=5, pady=5)
572 frame_sample.grid(row=0, column=1, rowspan=2, padx=5, pady=5,
573 sticky='nsew')
574 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
csabella9397e2a2017-07-30 13:34:25 -0400575 # frame_font.
576 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
577 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
578 font_name_title.pack(side=TOP, anchor=W)
579 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
580 scroll_font.pack(side=LEFT, fill=Y)
581 font_size_title.pack(side=LEFT, anchor=W)
582 self.sizelist.pack(side=LEFT, anchor=W)
583 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400584 # frame_sample.
csabella9397e2a2017-07-30 13:34:25 -0400585 self.font_sample.pack(expand=TRUE, fill=BOTH)
586 # frame_indent.
csabella9397e2a2017-07-30 13:34:25 -0400587 indent_title.pack(side=TOP, anchor=W, padx=5)
588 self.indent_scale.pack(side=TOP, padx=5, fill=X)
589
csabella9397e2a2017-07-30 13:34:25 -0400590 def load_font_cfg(self):
591 """Load current configuration settings for the font options.
592
593 Retrieve current font with idleConf.GetFont and font families
594 from tk. Setup fontlist and set font_name. Setup sizelist,
595 which sets font_size. Set font_bold. Call set_samples.
596 """
597 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
598 font_name = configured_font[0].lower()
599 font_size = configured_font[1]
600 font_bold = configured_font[2]=='bold'
601
602 # Set editor font selection list and font_name.
603 fonts = list(tkFont.families(self))
604 fonts.sort()
605 for font in fonts:
606 self.fontlist.insert(END, font)
607 self.font_name.set(font_name)
608 lc_fonts = [s.lower() for s in fonts]
609 try:
610 current_font_index = lc_fonts.index(font_name)
611 self.fontlist.see(current_font_index)
612 self.fontlist.select_set(current_font_index)
613 self.fontlist.select_anchor(current_font_index)
614 self.fontlist.activate(current_font_index)
615 except ValueError:
616 pass
617 # Set font size dropdown.
618 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
619 '16', '18', '20', '22', '25', '29', '34', '40'),
620 font_size)
621 # Set font weight.
622 self.font_bold.set(font_bold)
623 self.set_samples()
624
625 def var_changed_font(self, *params):
626 """Store changes to font attributes.
627
628 When one font attribute changes, save them all, as they are
629 not independent from each other. In particular, when we are
630 overriding the default font, we need to write out everything.
631 """
632 value = self.font_name.get()
633 changes.add_option('main', 'EditorWindow', 'font', value)
634 value = self.font_size.get()
635 changes.add_option('main', 'EditorWindow', 'font-size', value)
636 value = self.font_bold.get()
637 changes.add_option('main', 'EditorWindow', 'font-bold', value)
638 self.set_samples()
639
640 def on_fontlist_select(self, event):
641 """Handle selecting a font from the list.
642
643 Event can result from either mouse click or Up or Down key.
644 Set font_name and example displays to selection.
645 """
646 font = self.fontlist.get(
647 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
648 self.font_name.set(font.lower())
649
650 def set_samples(self, event=None):
651 """Update update both screen samples with the font settings.
652
653 Called on font initialization and change events.
654 Accesses font_name, font_size, and font_bold Variables.
Leo Ariasc3d95082018-02-03 18:36:10 -0600655 Updates font_sample and highlight page highlight_sample.
csabella9397e2a2017-07-30 13:34:25 -0400656 """
657 font_name = self.font_name.get()
658 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
659 new_font = (font_name, self.font_size.get(), font_weight)
660 self.font_sample['font'] = new_font
661 self.highlight_sample['font'] = new_font
662
663 def load_tab_cfg(self):
664 """Load current configuration settings for the tab options.
665
666 Attributes updated:
667 space_num: Set to value from idleConf.
668 """
669 # Set indent sizes.
670 space_num = idleConf.GetOption(
671 'main', 'Indent', 'num-spaces', default=4, type='int')
672 self.space_num.set(space_num)
673
674 def var_changed_space_num(self, *params):
675 "Store change to indentation size."
676 value = self.space_num.get()
677 changes.add_option('main', 'Indent', 'num-spaces', value)
678
679
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400680class HighPage(Frame):
681
682 def __init__(self, master):
683 super().__init__(master)
684 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400685 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400686 self.create_page_highlight()
687 self.load_theme_cfg()
688
689 def create_page_highlight(self):
690 """Return frame of widgets for Highlighting tab.
691
692 Enable users to provisionally change foreground and background
693 colors applied to textual tags. Color mappings are stored in
694 complete listings called themes. Built-in themes in
695 idlelib/config-highlight.def are fixed as far as the dialog is
696 concerned. Any theme can be used as the base for a new custom
697 theme, stored in .idlerc/config-highlight.cfg.
698
699 Function load_theme_cfg() initializes tk variables and theme
700 lists and calls paint_theme_sample() and set_highlight_target()
701 for the current theme. Radiobuttons builtin_theme_on and
702 custom_theme_on toggle var theme_source, which controls if the
703 current set of colors are from a builtin or custom theme.
704 DynOptionMenus builtinlist and customlist contain lists of the
705 builtin and custom themes, respectively, and the current item
706 from each list is stored in vars builtin_name and custom_name.
707
708 Function paint_theme_sample() applies the colors from the theme
709 to the tags in text widget highlight_sample and then invokes
710 set_color_sample(). Function set_highlight_target() sets the state
711 of the radiobuttons fg_on and bg_on based on the tag and it also
712 invokes set_color_sample().
713
714 Function set_color_sample() sets the background color for the frame
715 holding the color selector. This provides a larger visual of the
716 color for the current tag and plane (foreground/background).
717
718 Note: set_color_sample() is called from many places and is often
719 called more than once when a change is made. It is invoked when
720 foreground or background is selected (radiobuttons), from
721 paint_theme_sample() (theme is changed or load_cfg is called), and
722 from set_highlight_target() (target tag is changed or load_cfg called).
723
724 Button delete_custom invokes delete_custom() to delete
725 a custom theme from idleConf.userCfg['highlight'] and changes.
726 Button save_custom invokes save_as_new_theme() which calls
727 get_new_theme_name() and create_new() to save a custom theme
728 and its colors to idleConf.userCfg['highlight'].
729
730 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
731 if the current selected color for a tag is for the foreground or
732 background.
733
734 DynOptionMenu targetlist contains a readable description of the
735 tags applied to Python source within IDLE. Selecting one of the
736 tags from this list populates highlight_target, which has a callback
737 function set_highlight_target().
738
739 Text widget highlight_sample displays a block of text (which is
740 mock Python code) in which is embedded the defined tags and reflects
741 the color attributes of the current theme and changes for those tags.
742 Mouse button 1 allows for selection of a tag and updates
743 highlight_target with that tag value.
744
745 Note: The font in highlight_sample is set through the config in
746 the fonts tab.
747
748 In other words, a tag can be selected either from targetlist or
749 by clicking on the sample text within highlight_sample. The
750 plane (foreground/background) is selected via the radiobutton.
751 Together, these two (tag and plane) control what color is
752 shown in set_color_sample() for the current theme. Button set_color
753 invokes get_color() which displays a ColorChooser to change the
754 color for the selected tag/plane. If a new color is picked,
755 it will be saved to changes and the highlight_sample and
756 frame background will be updated.
757
758 Tk Variables:
759 color: Color of selected target.
760 builtin_name: Menu variable for built-in theme.
761 custom_name: Menu variable for custom theme.
762 fg_bg_toggle: Toggle for foreground/background color.
763 Note: this has no callback.
764 theme_source: Selector for built-in or custom theme.
765 highlight_target: Menu variable for the highlight tag target.
766
767 Instance Data Attributes:
768 theme_elements: Dictionary of tags for text highlighting.
769 The key is the display name and the value is a tuple of
770 (tag name, display sort order).
771
772 Methods [attachment]:
773 load_theme_cfg: Load current highlight colors.
774 get_color: Invoke colorchooser [button_set_color].
775 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
776 set_highlight_target: set fg_bg_toggle, set_color_sample().
777 set_color_sample: Set frame background to target.
778 on_new_color_set: Set new color and add option.
779 paint_theme_sample: Recolor sample.
780 get_new_theme_name: Get from popup.
781 create_new: Combine theme with changes and save.
782 save_as_new_theme: Save [button_save_custom].
783 set_theme_type: Command for [theme_source].
784 delete_custom: Activate default [button_delete_custom].
785 save_new: Save to userCfg['theme'] (is function).
786
787 Widgets of highlights page frame: (*) widgets bound to self
788 frame_custom: LabelFrame
789 (*)highlight_sample: Text
790 (*)frame_color_set: Frame
791 (*)button_set_color: Button
792 (*)targetlist: DynOptionMenu - highlight_target
793 frame_fg_bg_toggle: Frame
794 (*)fg_on: Radiobutton - fg_bg_toggle
795 (*)bg_on: Radiobutton - fg_bg_toggle
796 (*)button_save_custom: Button
797 frame_theme: LabelFrame
798 theme_type_title: Label
799 (*)builtin_theme_on: Radiobutton - theme_source
800 (*)custom_theme_on: Radiobutton - theme_source
801 (*)builtinlist: DynOptionMenu - builtin_name
802 (*)customlist: DynOptionMenu - custom_name
803 (*)button_delete_custom: Button
804 (*)theme_message: Label
805 """
806 self.theme_elements = {
807 'Normal Text': ('normal', '00'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400808 'Code Context': ('context', '01'),
809 'Python Keywords': ('keyword', '02'),
810 'Python Definitions': ('definition', '03'),
811 'Python Builtins': ('builtin', '04'),
812 'Python Comments': ('comment', '05'),
813 'Python Strings': ('string', '06'),
814 'Selected Text': ('hilite', '07'),
815 'Found Text': ('hit', '08'),
816 'Cursor': ('cursor', '09'),
817 'Editor Breakpoint': ('break', '10'),
818 'Shell Normal Text': ('console', '11'),
819 'Shell Error Text': ('error', '12'),
820 'Shell Stdout Text': ('stdout', '13'),
821 'Shell Stderr Text': ('stderr', '14'),
Tal Einat7123ea02019-07-23 15:22:11 +0300822 'Line Number': ('linenumber', '16'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400823 }
824 self.builtin_name = tracers.add(
825 StringVar(self), self.var_changed_builtin_name)
826 self.custom_name = tracers.add(
827 StringVar(self), self.var_changed_custom_name)
828 self.fg_bg_toggle = BooleanVar(self)
829 self.color = tracers.add(
830 StringVar(self), self.var_changed_color)
831 self.theme_source = tracers.add(
832 BooleanVar(self), self.var_changed_theme_source)
833 self.highlight_target = tracers.add(
834 StringVar(self), self.var_changed_highlight_target)
835
836 # Create widgets:
837 # body frame and section frames.
838 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
839 text=' Custom Highlighting ')
840 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
841 text=' Highlighting Theme ')
842 # frame_custom.
843 text = self.highlight_sample = Text(
844 frame_custom, relief=SOLID, borderwidth=1,
845 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
846 takefocus=FALSE, highlightthickness=0, wrap=NONE)
847 text.bind('<Double-Button-1>', lambda e: 'break')
848 text.bind('<B1-Motion>', lambda e: 'break')
wohlganger58fc71c2017-09-10 16:19:47 -0500849 text_and_tags=(
850 ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400851 ('#you can click here', 'comment'), ('\n', 'normal'),
852 ('#to choose items', 'comment'), ('\n', 'normal'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400853 ('code context section', 'context'), ('\n\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400854 ('def', 'keyword'), (' ', 'normal'),
855 ('func', 'definition'), ('(param):\n ', 'normal'),
856 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
857 ("'string'", 'string'), ('\n var1 = ', 'normal'),
858 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
859 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
860 ('list', 'builtin'), ('(', 'normal'),
861 ('None', 'keyword'), (')\n', 'normal'),
862 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
863 (' error ', 'error'), (' ', 'normal'),
864 ('cursor |', 'cursor'), ('\n ', 'normal'),
865 ('shell', 'console'), (' ', 'normal'),
866 ('stdout', 'stdout'), (' ', 'normal'),
867 ('stderr', 'stderr'), ('\n\n', 'normal'))
868 for texttag in text_and_tags:
869 text.insert(END, texttag[0], texttag[1])
Tal Einat7123ea02019-07-23 15:22:11 +0300870 n_lines = len(text.get('1.0', END).splitlines())
871 for lineno in range(1, n_lines + 1):
872 text.insert(f'{lineno}.0',
873 f'{lineno:{len(str(n_lines))}d} ',
874 'linenumber')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400875 for element in self.theme_elements:
876 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400877 # event.widget.winfo_top_level().highlight_target.set(elem)
878 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400879 text.tag_bind(
880 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400881 text['state'] = 'disabled'
882 self.style.configure('frame_color_set.TFrame', borderwidth=1,
883 relief='solid')
884 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400885 frame_fg_bg_toggle = Frame(frame_custom)
886 self.button_set_color = Button(
887 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400888 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400889 self.targetlist = DynOptionMenu(
890 self.frame_color_set, self.highlight_target, None,
891 highlightthickness=0) #, command=self.set_highlight_targetBinding
892 self.fg_on = Radiobutton(
893 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
894 text='Foreground', command=self.set_color_sample_binding)
895 self.bg_on = Radiobutton(
896 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
897 text='Background', command=self.set_color_sample_binding)
898 self.fg_bg_toggle.set(1)
899 self.button_save_custom = Button(
900 frame_custom, text='Save as New Custom Theme',
901 command=self.save_as_new_theme)
902 # frame_theme.
903 theme_type_title = Label(frame_theme, text='Select : ')
904 self.builtin_theme_on = Radiobutton(
905 frame_theme, variable=self.theme_source, value=1,
906 command=self.set_theme_type, text='a Built-in Theme')
907 self.custom_theme_on = Radiobutton(
908 frame_theme, variable=self.theme_source, value=0,
909 command=self.set_theme_type, text='a Custom Theme')
910 self.builtinlist = DynOptionMenu(
911 frame_theme, self.builtin_name, None, command=None)
912 self.customlist = DynOptionMenu(
913 frame_theme, self.custom_name, None, command=None)
914 self.button_delete_custom = Button(
915 frame_theme, text='Delete Custom Theme',
916 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400917 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400918 # Pack widgets:
919 # body.
920 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500921 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400922 # frame_custom.
923 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
924 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
925 self.highlight_sample.pack(
926 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
927 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
928 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
929 self.fg_on.pack(side=LEFT, anchor=E)
930 self.bg_on.pack(side=RIGHT, anchor=W)
931 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
932 # frame_theme.
933 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
934 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
935 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
936 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
937 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
938 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
939 self.theme_message.pack(side=TOP, fill=X, pady=5)
940
941 def load_theme_cfg(self):
942 """Load current configuration settings for the theme options.
943
944 Based on the theme_source toggle, the theme is set as
945 either builtin or custom and the initial widget values
946 reflect the current settings from idleConf.
947
948 Attributes updated:
949 theme_source: Set from idleConf.
950 builtinlist: List of default themes from idleConf.
951 customlist: List of custom themes from idleConf.
952 custom_theme_on: Disabled if there are no custom themes.
953 custom_theme: Message with additional information.
954 targetlist: Create menu from self.theme_elements.
955
956 Methods:
957 set_theme_type
958 paint_theme_sample
959 set_highlight_target
960 """
961 # Set current theme type radiobutton.
962 self.theme_source.set(idleConf.GetOption(
963 'main', 'Theme', 'default', type='bool', default=1))
964 # Set current theme.
965 current_option = idleConf.CurrentTheme()
966 # Load available theme option menus.
967 if self.theme_source.get(): # Default theme selected.
968 item_list = idleConf.GetSectionList('default', 'highlight')
969 item_list.sort()
970 self.builtinlist.SetMenu(item_list, current_option)
971 item_list = idleConf.GetSectionList('user', 'highlight')
972 item_list.sort()
973 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400974 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400975 self.custom_name.set('- no custom themes -')
976 else:
977 self.customlist.SetMenu(item_list, item_list[0])
978 else: # User theme selected.
979 item_list = idleConf.GetSectionList('user', 'highlight')
980 item_list.sort()
981 self.customlist.SetMenu(item_list, current_option)
982 item_list = idleConf.GetSectionList('default', 'highlight')
983 item_list.sort()
984 self.builtinlist.SetMenu(item_list, item_list[0])
985 self.set_theme_type()
986 # Load theme element option menu.
987 theme_names = list(self.theme_elements.keys())
988 theme_names.sort(key=lambda x: self.theme_elements[x][1])
989 self.targetlist.SetMenu(theme_names, theme_names[0])
990 self.paint_theme_sample()
991 self.set_highlight_target()
992
993 def var_changed_builtin_name(self, *params):
994 """Process new builtin theme selection.
995
996 Add the changed theme's name to the changed_items and recreate
997 the sample with the values from the selected theme.
998 """
999 old_themes = ('IDLE Classic', 'IDLE New')
1000 value = self.builtin_name.get()
1001 if value not in old_themes:
1002 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
1003 changes.add_option('main', 'Theme', 'name', old_themes[0])
1004 changes.add_option('main', 'Theme', 'name2', value)
1005 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001006 else:
1007 changes.add_option('main', 'Theme', 'name', value)
1008 changes.add_option('main', 'Theme', 'name2', '')
1009 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001010 self.paint_theme_sample()
1011
1012 def var_changed_custom_name(self, *params):
1013 """Process new custom theme selection.
1014
1015 If a new custom theme is selected, add the name to the
1016 changed_items and apply the theme to the sample.
1017 """
1018 value = self.custom_name.get()
1019 if value != '- no custom themes -':
1020 changes.add_option('main', 'Theme', 'name', value)
1021 self.paint_theme_sample()
1022
1023 def var_changed_theme_source(self, *params):
1024 """Process toggle between builtin and custom theme.
1025
1026 Update the default toggle value and apply the newly
1027 selected theme type.
1028 """
1029 value = self.theme_source.get()
1030 changes.add_option('main', 'Theme', 'default', value)
1031 if value:
1032 self.var_changed_builtin_name()
1033 else:
1034 self.var_changed_custom_name()
1035
1036 def var_changed_color(self, *params):
1037 "Process change to color choice."
1038 self.on_new_color_set()
1039
1040 def var_changed_highlight_target(self, *params):
1041 "Process selection of new target tag for highlighting."
1042 self.set_highlight_target()
1043
1044 def set_theme_type(self):
1045 """Set available screen options based on builtin or custom theme.
1046
1047 Attributes accessed:
1048 theme_source
1049
1050 Attributes updated:
1051 builtinlist
1052 customlist
1053 button_delete_custom
1054 custom_theme_on
1055
1056 Called from:
1057 handler for builtin_theme_on and custom_theme_on
1058 delete_custom
1059 create_new
1060 load_theme_cfg
1061 """
1062 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001063 self.builtinlist['state'] = 'normal'
1064 self.customlist['state'] = 'disabled'
1065 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001066 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001067 self.builtinlist['state'] = 'disabled'
1068 self.custom_theme_on.state(('!disabled',))
1069 self.customlist['state'] = 'normal'
1070 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001071
1072 def get_color(self):
1073 """Handle button to select a new color for the target tag.
1074
1075 If a new color is selected while using a builtin theme, a
1076 name must be supplied to create a custom theme.
1077
1078 Attributes accessed:
1079 highlight_target
1080 frame_color_set
1081 theme_source
1082
1083 Attributes updated:
1084 color
1085
1086 Methods:
1087 get_new_theme_name
1088 create_new
1089 """
1090 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001091 prev_color = self.style.lookup(self.frame_color_set['style'],
1092 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001093 rgbTuplet, color_string = tkColorChooser.askcolor(
1094 parent=self, title='Pick new color for : '+target,
1095 initialcolor=prev_color)
1096 if color_string and (color_string != prev_color):
1097 # User didn't cancel and they chose a new color.
1098 if self.theme_source.get(): # Current theme is a built-in.
1099 message = ('Your changes will be saved as a new Custom Theme. '
1100 'Enter a name for your new Custom Theme below.')
1101 new_theme = self.get_new_theme_name(message)
1102 if not new_theme: # User cancelled custom theme creation.
1103 return
1104 else: # Create new custom theme based on previously active theme.
1105 self.create_new(new_theme)
1106 self.color.set(color_string)
1107 else: # Current theme is user defined.
1108 self.color.set(color_string)
1109
1110 def on_new_color_set(self):
1111 "Display sample of new color selection on the dialog."
1112 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001113 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001114 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1115 sample_element = self.theme_elements[self.highlight_target.get()][0]
1116 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1117 theme = self.custom_name.get()
1118 theme_element = sample_element + '-' + plane
1119 changes.add_option('highlight', theme, theme_element, new_color)
1120
1121 def get_new_theme_name(self, message):
1122 "Return name of new theme from query popup."
1123 used_names = (idleConf.GetSectionList('user', 'highlight') +
1124 idleConf.GetSectionList('default', 'highlight'))
1125 new_theme = SectionName(
1126 self, 'New Custom Theme', message, used_names).result
1127 return new_theme
1128
1129 def save_as_new_theme(self):
1130 """Prompt for new theme name and create the theme.
1131
1132 Methods:
1133 get_new_theme_name
1134 create_new
1135 """
1136 new_theme_name = self.get_new_theme_name('New Theme Name:')
1137 if new_theme_name:
1138 self.create_new(new_theme_name)
1139
1140 def create_new(self, new_theme_name):
1141 """Create a new custom theme with the given name.
1142
1143 Create the new theme based on the previously active theme
1144 with the current changes applied. Once it is saved, then
1145 activate the new theme.
1146
1147 Attributes accessed:
1148 builtin_name
1149 custom_name
1150
1151 Attributes updated:
1152 customlist
1153 theme_source
1154
1155 Method:
1156 save_new
1157 set_theme_type
1158 """
1159 if self.theme_source.get():
1160 theme_type = 'default'
1161 theme_name = self.builtin_name.get()
1162 else:
1163 theme_type = 'user'
1164 theme_name = self.custom_name.get()
1165 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1166 # Apply any of the old theme's unsaved changes to the new theme.
1167 if theme_name in changes['highlight']:
1168 theme_changes = changes['highlight'][theme_name]
1169 for element in theme_changes:
1170 new_theme[element] = theme_changes[element]
1171 # Save the new theme.
1172 self.save_new(new_theme_name, new_theme)
1173 # Change GUI over to the new theme.
1174 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1175 custom_theme_list.sort()
1176 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1177 self.theme_source.set(0)
1178 self.set_theme_type()
1179
1180 def set_highlight_target(self):
1181 """Set fg/bg toggle and color based on highlight tag target.
1182
1183 Instance variables accessed:
1184 highlight_target
1185
1186 Attributes updated:
1187 fg_on
1188 bg_on
1189 fg_bg_toggle
1190
1191 Methods:
1192 set_color_sample
1193
1194 Called from:
1195 var_changed_highlight_target
1196 load_theme_cfg
1197 """
1198 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001199 self.fg_on.state(('disabled',))
1200 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001201 self.fg_bg_toggle.set(1)
1202 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001203 self.fg_on.state(('!disabled',))
1204 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001205 self.fg_bg_toggle.set(1)
1206 self.set_color_sample()
1207
1208 def set_color_sample_binding(self, *args):
1209 """Change color sample based on foreground/background toggle.
1210
1211 Methods:
1212 set_color_sample
1213 """
1214 self.set_color_sample()
1215
1216 def set_color_sample(self):
1217 """Set the color of the frame background to reflect the selected target.
1218
1219 Instance variables accessed:
1220 theme_elements
1221 highlight_target
1222 fg_bg_toggle
1223 highlight_sample
1224
1225 Attributes updated:
1226 frame_color_set
1227 """
1228 # Set the color sample area.
1229 tag = self.theme_elements[self.highlight_target.get()][0]
1230 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1231 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001232 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001233
1234 def paint_theme_sample(self):
1235 """Apply the theme colors to each element tag in the sample text.
1236
1237 Instance attributes accessed:
1238 theme_elements
1239 theme_source
1240 builtin_name
1241 custom_name
1242
1243 Attributes updated:
1244 highlight_sample: Set the tag elements to the theme.
1245
1246 Methods:
1247 set_color_sample
1248
1249 Called from:
1250 var_changed_builtin_name
1251 var_changed_custom_name
1252 load_theme_cfg
1253 """
1254 if self.theme_source.get(): # Default theme
1255 theme = self.builtin_name.get()
1256 else: # User theme
1257 theme = self.custom_name.get()
1258 for element_title in self.theme_elements:
1259 element = self.theme_elements[element_title][0]
1260 colors = idleConf.GetHighlight(theme, element)
1261 if element == 'cursor': # Cursor sample needs special painting.
1262 colors['background'] = idleConf.GetHighlight(
Terry Jan Reedyc1419572019-03-22 18:23:41 -04001263 theme, 'normal')['background']
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001264 # Handle any unsaved changes to this theme.
1265 if theme in changes['highlight']:
1266 theme_dict = changes['highlight'][theme]
1267 if element + '-foreground' in theme_dict:
1268 colors['foreground'] = theme_dict[element + '-foreground']
1269 if element + '-background' in theme_dict:
1270 colors['background'] = theme_dict[element + '-background']
1271 self.highlight_sample.tag_config(element, **colors)
1272 self.set_color_sample()
1273
1274 def save_new(self, theme_name, theme):
1275 """Save a newly created theme to idleConf.
1276
1277 theme_name - string, the name of the new theme
1278 theme - dictionary containing the new theme
1279 """
1280 if not idleConf.userCfg['highlight'].has_section(theme_name):
1281 idleConf.userCfg['highlight'].add_section(theme_name)
1282 for element in theme:
1283 value = theme[element]
1284 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1285
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001286 def askyesno(self, *args, **kwargs):
1287 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001288 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001289
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001290 def delete_custom(self):
1291 """Handle event to delete custom theme.
1292
1293 The current theme is deactivated and the default theme is
1294 activated. The custom theme is permanently removed from
1295 the config file.
1296
1297 Attributes accessed:
1298 custom_name
1299
1300 Attributes updated:
1301 custom_theme_on
1302 customlist
1303 theme_source
1304 builtin_name
1305
1306 Methods:
1307 deactivate_current_config
1308 save_all_changed_extensions
1309 activate_config_changes
1310 set_theme_type
1311 """
1312 theme_name = self.custom_name.get()
1313 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001314 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001315 'Delete Theme', delmsg % theme_name, parent=self):
1316 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001317 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001318 # Remove theme from changes, config, and file.
1319 changes.delete_section('highlight', theme_name)
1320 # Reload user theme list.
1321 item_list = idleConf.GetSectionList('user', 'highlight')
1322 item_list.sort()
1323 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001324 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001325 self.customlist.SetMenu(item_list, '- no custom themes -')
1326 else:
1327 self.customlist.SetMenu(item_list, item_list[0])
1328 # Revert to default theme.
1329 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1330 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1331 # User can't back out of these changes, they must be applied now.
1332 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001333 self.cd.save_all_changed_extensions()
1334 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001335 self.set_theme_type()
1336
1337
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001338class KeysPage(Frame):
1339
1340 def __init__(self, master):
1341 super().__init__(master)
1342 self.cd = master.master
1343 self.create_page_keys()
1344 self.load_key_cfg()
1345
1346 def create_page_keys(self):
1347 """Return frame of widgets for Keys tab.
1348
1349 Enable users to provisionally change both individual and sets of
1350 keybindings (shortcut keys). Except for features implemented as
1351 extensions, keybindings are stored in complete sets called
1352 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1353 as far as the dialog is concerned. Any keyset can be used as the
1354 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1355
1356 Function load_key_cfg() initializes tk variables and keyset
1357 lists and calls load_keys_list for the current keyset.
1358 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1359 keyset_source, which controls if the current set of keybindings
1360 are from a builtin or custom keyset. DynOptionMenus builtinlist
1361 and customlist contain lists of the builtin and custom keysets,
1362 respectively, and the current item from each list is stored in
1363 vars builtin_name and custom_name.
1364
1365 Button delete_custom_keys invokes delete_custom_keys() to delete
1366 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1367 save_custom_keys invokes save_as_new_key_set() which calls
1368 get_new_keys_name() and create_new_key_set() to save a custom keyset
1369 and its keybindings to idleConf.userCfg['keys'].
1370
1371 Listbox bindingslist contains all of the keybindings for the
1372 selected keyset. The keybindings are loaded in load_keys_list()
1373 and are pairs of (event, [keys]) where keys can be a list
1374 of one or more key combinations to bind to the same event.
1375 Mouse button 1 click invokes on_bindingslist_select(), which
1376 allows button_new_keys to be clicked.
1377
1378 So, an item is selected in listbindings, which activates
1379 button_new_keys, and clicking button_new_keys calls function
1380 get_new_keys(). Function get_new_keys() gets the key mappings from the
1381 current keyset for the binding event item that was selected. The
1382 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001383 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001384 to be entered for that binding event. If the keys aren't
1385 changed, nothing happens. If the keys are changed and the keyset
1386 is a builtin, function get_new_keys_name() will be called
1387 for input of a custom keyset name. If no name is given, then the
1388 change to the keybinding will abort and no updates will be made. If
1389 a custom name is entered in the prompt or if the current keyset was
1390 already custom (and thus didn't require a prompt), then
1391 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1392 with the change to the event binding. The item listing in bindingslist
1393 is updated with the new keys. Var keybinding is also set which invokes
1394 the callback function, var_changed_keybinding, to add the change to
1395 the 'keys' or 'extensions' changes tracker based on the binding type.
1396
1397 Tk Variables:
1398 keybinding: Action/key bindings.
1399
1400 Methods:
1401 load_keys_list: Reload active set.
1402 create_new_key_set: Combine active keyset and changes.
1403 set_keys_type: Command for keyset_source.
1404 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1405 deactivate_current_config: Remove keys bindings in editors.
1406
1407 Widgets for KeysPage(frame): (*) widgets bound to self
1408 frame_key_sets: LabelFrame
1409 frames[0]: Frame
1410 (*)builtin_keyset_on: Radiobutton - var keyset_source
1411 (*)custom_keyset_on: Radiobutton - var keyset_source
1412 (*)builtinlist: DynOptionMenu - var builtin_name,
1413 func keybinding_selected
1414 (*)customlist: DynOptionMenu - var custom_name,
1415 func keybinding_selected
1416 (*)keys_message: Label
1417 frames[1]: Frame
1418 (*)button_delete_custom_keys: Button - delete_custom_keys
1419 (*)button_save_custom_keys: Button - save_as_new_key_set
1420 frame_custom: LabelFrame
1421 frame_target: Frame
1422 target_title: Label
1423 scroll_target_y: Scrollbar
1424 scroll_target_x: Scrollbar
1425 (*)bindingslist: ListBox - on_bindingslist_select
1426 (*)button_new_keys: Button - get_new_keys & ..._name
1427 """
1428 self.builtin_name = tracers.add(
1429 StringVar(self), self.var_changed_builtin_name)
1430 self.custom_name = tracers.add(
1431 StringVar(self), self.var_changed_custom_name)
1432 self.keyset_source = tracers.add(
1433 BooleanVar(self), self.var_changed_keyset_source)
1434 self.keybinding = tracers.add(
1435 StringVar(self), self.var_changed_keybinding)
1436
1437 # Create widgets:
1438 # body and section frames.
1439 frame_custom = LabelFrame(
1440 self, borderwidth=2, relief=GROOVE,
1441 text=' Custom Key Bindings ')
1442 frame_key_sets = LabelFrame(
1443 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1444 # frame_custom.
1445 frame_target = Frame(frame_custom)
1446 target_title = Label(frame_target, text='Action - Key(s)')
1447 scroll_target_y = Scrollbar(frame_target)
1448 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1449 self.bindingslist = Listbox(
1450 frame_target, takefocus=FALSE, exportselection=FALSE)
1451 self.bindingslist.bind('<ButtonRelease-1>',
1452 self.on_bindingslist_select)
1453 scroll_target_y['command'] = self.bindingslist.yview
1454 scroll_target_x['command'] = self.bindingslist.xview
1455 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1456 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1457 self.button_new_keys = Button(
1458 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001459 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001460 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001461 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001462 for i in range(2)]
1463 self.builtin_keyset_on = Radiobutton(
1464 frames[0], variable=self.keyset_source, value=1,
1465 command=self.set_keys_type, text='Use a Built-in Key Set')
1466 self.custom_keyset_on = Radiobutton(
1467 frames[0], variable=self.keyset_source, value=0,
1468 command=self.set_keys_type, text='Use a Custom Key Set')
1469 self.builtinlist = DynOptionMenu(
1470 frames[0], self.builtin_name, None, command=None)
1471 self.customlist = DynOptionMenu(
1472 frames[0], self.custom_name, None, command=None)
1473 self.button_delete_custom_keys = Button(
1474 frames[1], text='Delete Custom Key Set',
1475 command=self.delete_custom_keys)
1476 self.button_save_custom_keys = Button(
1477 frames[1], text='Save as New Custom Key Set',
1478 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001479 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001480
1481 # Pack widgets:
1482 # body.
1483 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1484 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1485 # frame_custom.
1486 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1487 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1488 # frame_target.
1489 frame_target.columnconfigure(0, weight=1)
1490 frame_target.rowconfigure(1, weight=1)
1491 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1492 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1493 scroll_target_y.grid(row=1, column=1, sticky=NS)
1494 scroll_target_x.grid(row=2, column=0, sticky=EW)
1495 # frame_key_sets.
1496 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1497 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1498 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1499 self.customlist.grid(row=1, column=1, sticky=NSEW)
1500 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1501 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1502 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1503 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1504 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1505
1506 def load_key_cfg(self):
1507 "Load current configuration settings for the keybinding options."
1508 # Set current keys type radiobutton.
1509 self.keyset_source.set(idleConf.GetOption(
1510 'main', 'Keys', 'default', type='bool', default=1))
1511 # Set current keys.
1512 current_option = idleConf.CurrentKeys()
1513 # Load available keyset option menus.
1514 if self.keyset_source.get(): # Default theme selected.
1515 item_list = idleConf.GetSectionList('default', 'keys')
1516 item_list.sort()
1517 self.builtinlist.SetMenu(item_list, current_option)
1518 item_list = idleConf.GetSectionList('user', 'keys')
1519 item_list.sort()
1520 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001521 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001522 self.custom_name.set('- no custom keys -')
1523 else:
1524 self.customlist.SetMenu(item_list, item_list[0])
1525 else: # User key set selected.
1526 item_list = idleConf.GetSectionList('user', 'keys')
1527 item_list.sort()
1528 self.customlist.SetMenu(item_list, current_option)
1529 item_list = idleConf.GetSectionList('default', 'keys')
1530 item_list.sort()
1531 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1532 self.set_keys_type()
1533 # Load keyset element list.
1534 keyset_name = idleConf.CurrentKeys()
1535 self.load_keys_list(keyset_name)
1536
1537 def var_changed_builtin_name(self, *params):
1538 "Process selection of builtin key set."
1539 old_keys = (
1540 'IDLE Classic Windows',
1541 'IDLE Classic Unix',
1542 'IDLE Classic Mac',
1543 'IDLE Classic OSX',
1544 )
1545 value = self.builtin_name.get()
1546 if value not in old_keys:
1547 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1548 changes.add_option('main', 'Keys', 'name', old_keys[0])
1549 changes.add_option('main', 'Keys', 'name2', value)
1550 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001551 else:
1552 changes.add_option('main', 'Keys', 'name', value)
1553 changes.add_option('main', 'Keys', 'name2', '')
1554 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001555 self.load_keys_list(value)
1556
1557 def var_changed_custom_name(self, *params):
1558 "Process selection of custom key set."
1559 value = self.custom_name.get()
1560 if value != '- no custom keys -':
1561 changes.add_option('main', 'Keys', 'name', value)
1562 self.load_keys_list(value)
1563
1564 def var_changed_keyset_source(self, *params):
1565 "Process toggle between builtin key set and custom key set."
1566 value = self.keyset_source.get()
1567 changes.add_option('main', 'Keys', 'default', value)
1568 if value:
1569 self.var_changed_builtin_name()
1570 else:
1571 self.var_changed_custom_name()
1572
1573 def var_changed_keybinding(self, *params):
1574 "Store change to a keybinding."
1575 value = self.keybinding.get()
1576 key_set = self.custom_name.get()
1577 event = self.bindingslist.get(ANCHOR).split()[0]
1578 if idleConf.IsCoreBinding(event):
1579 changes.add_option('keys', key_set, event, value)
1580 else: # Event is an extension binding.
1581 ext_name = idleConf.GetExtnNameForEvent(event)
1582 ext_keybind_section = ext_name + '_cfgBindings'
1583 changes.add_option('extensions', ext_keybind_section, event, value)
1584
1585 def set_keys_type(self):
1586 "Set available screen options based on builtin or custom key set."
1587 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001588 self.builtinlist['state'] = 'normal'
1589 self.customlist['state'] = 'disabled'
1590 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001591 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001592 self.builtinlist['state'] = 'disabled'
1593 self.custom_keyset_on.state(('!disabled',))
1594 self.customlist['state'] = 'normal'
1595 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001596
1597 def get_new_keys(self):
1598 """Handle event to change key binding for selected line.
1599
1600 A selection of a key/binding in the list of current
1601 bindings pops up a dialog to enter a new binding. If
1602 the current key set is builtin and a binding has
1603 changed, then a name for a custom key set needs to be
1604 entered for the change to be applied.
1605 """
1606 list_index = self.bindingslist.index(ANCHOR)
1607 binding = self.bindingslist.get(list_index)
1608 bind_name = binding.split()[0]
1609 if self.keyset_source.get():
1610 current_key_set_name = self.builtin_name.get()
1611 else:
1612 current_key_set_name = self.custom_name.get()
1613 current_bindings = idleConf.GetCurrentKeySet()
1614 if current_key_set_name in changes['keys']: # unsaved changes
1615 key_set_changes = changes['keys'][current_key_set_name]
1616 for event in key_set_changes:
1617 current_bindings[event] = key_set_changes[event].split()
1618 current_key_sequences = list(current_bindings.values())
1619 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1620 current_key_sequences).result
1621 if new_keys:
1622 if self.keyset_source.get(): # Current key set is a built-in.
1623 message = ('Your changes will be saved as a new Custom Key Set.'
1624 ' Enter a name for your new Custom Key Set below.')
1625 new_keyset = self.get_new_keys_name(message)
1626 if not new_keyset: # User cancelled custom key set creation.
1627 self.bindingslist.select_set(list_index)
1628 self.bindingslist.select_anchor(list_index)
1629 return
1630 else: # Create new custom key set based on previously active key set.
1631 self.create_new_key_set(new_keyset)
1632 self.bindingslist.delete(list_index)
1633 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1634 self.bindingslist.select_set(list_index)
1635 self.bindingslist.select_anchor(list_index)
1636 self.keybinding.set(new_keys)
1637 else:
1638 self.bindingslist.select_set(list_index)
1639 self.bindingslist.select_anchor(list_index)
1640
1641 def get_new_keys_name(self, message):
1642 "Return new key set name from query popup."
1643 used_names = (idleConf.GetSectionList('user', 'keys') +
1644 idleConf.GetSectionList('default', 'keys'))
1645 new_keyset = SectionName(
1646 self, 'New Custom Key Set', message, used_names).result
1647 return new_keyset
1648
1649 def save_as_new_key_set(self):
1650 "Prompt for name of new key set and save changes using that name."
1651 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1652 if new_keys_name:
1653 self.create_new_key_set(new_keys_name)
1654
1655 def on_bindingslist_select(self, event):
1656 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001657 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001658
1659 def create_new_key_set(self, new_key_set_name):
1660 """Create a new custom key set with the given name.
1661
1662 Copy the bindings/keys from the previously active keyset
1663 to the new keyset and activate the new custom keyset.
1664 """
1665 if self.keyset_source.get():
1666 prev_key_set_name = self.builtin_name.get()
1667 else:
1668 prev_key_set_name = self.custom_name.get()
1669 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1670 new_keys = {}
1671 for event in prev_keys: # Add key set to changed items.
1672 event_name = event[2:-2] # Trim off the angle brackets.
1673 binding = ' '.join(prev_keys[event])
1674 new_keys[event_name] = binding
1675 # Handle any unsaved changes to prev key set.
1676 if prev_key_set_name in changes['keys']:
1677 key_set_changes = changes['keys'][prev_key_set_name]
1678 for event in key_set_changes:
1679 new_keys[event] = key_set_changes[event]
1680 # Save the new key set.
1681 self.save_new_key_set(new_key_set_name, new_keys)
1682 # Change GUI over to the new key set.
1683 custom_key_list = idleConf.GetSectionList('user', 'keys')
1684 custom_key_list.sort()
1685 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1686 self.keyset_source.set(0)
1687 self.set_keys_type()
1688
1689 def load_keys_list(self, keyset_name):
1690 """Reload the list of action/key binding pairs for the active key set.
1691
1692 An action/key binding can be selected to change the key binding.
1693 """
1694 reselect = False
1695 if self.bindingslist.curselection():
1696 reselect = True
1697 list_index = self.bindingslist.index(ANCHOR)
1698 keyset = idleConf.GetKeySet(keyset_name)
1699 bind_names = list(keyset.keys())
1700 bind_names.sort()
1701 self.bindingslist.delete(0, END)
1702 for bind_name in bind_names:
1703 key = ' '.join(keyset[bind_name])
1704 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1705 if keyset_name in changes['keys']:
1706 # Handle any unsaved changes to this key set.
1707 if bind_name in changes['keys'][keyset_name]:
1708 key = changes['keys'][keyset_name][bind_name]
1709 self.bindingslist.insert(END, bind_name+' - '+key)
1710 if reselect:
1711 self.bindingslist.see(list_index)
1712 self.bindingslist.select_set(list_index)
1713 self.bindingslist.select_anchor(list_index)
1714
1715 @staticmethod
1716 def save_new_key_set(keyset_name, keyset):
1717 """Save a newly created core key set.
1718
1719 Add keyset to idleConf.userCfg['keys'], not to disk.
1720 If the keyset doesn't exist, it is created. The
1721 binding/keys are taken from the keyset argument.
1722
1723 keyset_name - string, the name of the new key set
1724 keyset - dictionary containing the new keybindings
1725 """
1726 if not idleConf.userCfg['keys'].has_section(keyset_name):
1727 idleConf.userCfg['keys'].add_section(keyset_name)
1728 for event in keyset:
1729 value = keyset[event]
1730 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1731
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001732 def askyesno(self, *args, **kwargs):
1733 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001734 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001735
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001736 def delete_custom_keys(self):
1737 """Handle event to delete a custom key set.
1738
1739 Applying the delete deactivates the current configuration and
1740 reverts to the default. The custom key set is permanently
1741 deleted from the config file.
1742 """
1743 keyset_name = self.custom_name.get()
1744 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001745 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001746 'Delete Key Set', delmsg % keyset_name, parent=self):
1747 return
1748 self.cd.deactivate_current_config()
1749 # Remove key set from changes, config, and file.
1750 changes.delete_section('keys', keyset_name)
1751 # Reload user key set list.
1752 item_list = idleConf.GetSectionList('user', 'keys')
1753 item_list.sort()
1754 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001755 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001756 self.customlist.SetMenu(item_list, '- no custom keys -')
1757 else:
1758 self.customlist.SetMenu(item_list, item_list[0])
1759 # Revert to default key set.
1760 self.keyset_source.set(idleConf.defaultCfg['main']
Tal Einat604e7b92018-09-25 15:10:14 +03001761 .Get('Keys', 'default'))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001762 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
Tal Einat604e7b92018-09-25 15:10:14 +03001763 or idleConf.default_keys())
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001764 # User can't back out of these changes, they must be applied now.
1765 changes.save_all()
1766 self.cd.save_all_changed_extensions()
1767 self.cd.activate_config_changes()
1768 self.set_keys_type()
1769
1770
csabellae8eb17b2017-07-30 18:39:17 -04001771class GenPage(Frame):
1772
csabella6f446be2017-08-01 00:24:07 -04001773 def __init__(self, master):
1774 super().__init__(master)
Tal Einat1ebee372019-07-23 13:02:11 +03001775
1776 self.init_validators()
csabellae8eb17b2017-07-30 18:39:17 -04001777 self.create_page_general()
1778 self.load_general_cfg()
1779
Tal Einat1ebee372019-07-23 13:02:11 +03001780 def init_validators(self):
1781 digits_or_empty_re = re.compile(r'[0-9]*')
1782 def is_digits_or_empty(s):
1783 "Return 's is blank or contains only digits'"
1784 return digits_or_empty_re.fullmatch(s) is not None
1785 self.digits_only = (self.register(is_digits_or_empty), '%P',)
1786
csabellae8eb17b2017-07-30 18:39:17 -04001787 def create_page_general(self):
1788 """Return frame of widgets for General tab.
1789
1790 Enable users to provisionally change general options. Function
1791 load_general_cfg intializes tk variables and helplist using
1792 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1793 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1794 set var autosave. Entry boxes win_width_int and win_height_int
1795 set var win_width and win_height. Setting var_name invokes the
1796 default callback that adds option to changes.
1797
1798 Helplist: load_general_cfg loads list user_helplist with
1799 name, position pairs and copies names to listbox helplist.
1800 Clicking a name invokes help_source selected. Clicking
1801 button_helplist_name invokes helplist_item_name, which also
1802 changes user_helplist. These functions all call
1803 set_add_delete_state. All but load call update_help_changes to
1804 rewrite changes['main']['HelpFiles'].
1805
Cheryl Sabella2f896462017-08-14 21:21:43 -04001806 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001807 frame_window: LabelFrame
1808 frame_run: Frame
1809 startup_title: Label
1810 (*)startup_editor_on: Radiobutton - startup_edit
1811 (*)startup_shell_on: Radiobutton - startup_edit
1812 frame_win_size: Frame
1813 win_size_title: Label
1814 win_width_title: Label
1815 (*)win_width_int: Entry - win_width
1816 win_height_title: Label
1817 (*)win_height_int: Entry - win_height
Cheryl Sabella845d8642018-02-04 18:15:21 -05001818 frame_autocomplete: Frame
1819 auto_wait_title: Label
1820 (*)auto_wait_int: Entry - autocomplete_wait
1821 frame_paren1: Frame
1822 paren_style_title: Label
1823 (*)paren_style_type: OptionMenu - paren_style
1824 frame_paren2: Frame
1825 paren_time_title: Label
1826 (*)paren_flash_time: Entry - flash_delay
1827 (*)bell_on: Checkbutton - paren_bell
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001828 frame_editor: LabelFrame
1829 frame_save: Frame
1830 run_save_title: Label
1831 (*)save_ask_on: Radiobutton - autosave
1832 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella845d8642018-02-04 18:15:21 -05001833 frame_format: Frame
1834 format_width_title: Label
1835 (*)format_width_int: Entry - format_width
Tal Einat7123ea02019-07-23 15:22:11 +03001836 frame_line_numbers_default: Frame
1837 line_numbers_default_title: Label
1838 (*)line_numbers_default_bool: Checkbutton - line_numbers_default
Cheryl Sabella845d8642018-02-04 18:15:21 -05001839 frame_context: Frame
1840 context_title: Label
1841 (*)context_int: Entry - context_lines
Tal Einat604e7b92018-09-25 15:10:14 +03001842 frame_shell: LabelFrame
1843 frame_auto_squeeze_min_lines: Frame
1844 auto_squeeze_min_lines_title: Label
1845 (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines
Cheryl Sabella2f896462017-08-14 21:21:43 -04001846 frame_help: LabelFrame
1847 frame_helplist: Frame
1848 frame_helplist_buttons: Frame
1849 (*)button_helplist_edit
1850 (*)button_helplist_add
1851 (*)button_helplist_remove
1852 (*)helplist: ListBox
1853 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001854 """
wohlganger58fc71c2017-09-10 16:19:47 -05001855 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001856 self.startup_edit = tracers.add(
1857 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001858 self.win_width = tracers.add(
1859 StringVar(self), ('main', 'EditorWindow', 'width'))
1860 self.win_height = tracers.add(
1861 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001862 self.autocomplete_wait = tracers.add(
1863 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1864 self.paren_style = tracers.add(
1865 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1866 self.flash_delay = tracers.add(
1867 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1868 self.paren_bell = tracers.add(
1869 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001870
Tal Einat604e7b92018-09-25 15:10:14 +03001871 self.auto_squeeze_min_lines = tracers.add(
1872 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
1873
wohlganger58fc71c2017-09-10 16:19:47 -05001874 self.autosave = tracers.add(
1875 IntVar(self), ('main', 'General', 'autosave'))
1876 self.format_width = tracers.add(
1877 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
Tal Einat7123ea02019-07-23 15:22:11 +03001878 self.line_numbers_default = tracers.add(
1879 BooleanVar(self),
1880 ('main', 'EditorWindow', 'line-numbers-default'))
wohlganger58fc71c2017-09-10 16:19:47 -05001881 self.context_lines = tracers.add(
Cheryl Sabella29996a12018-06-01 19:23:00 -04001882 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
wohlganger58fc71c2017-09-10 16:19:47 -05001883
1884 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001885 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001886 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1887 text=' Window Preferences')
1888 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1889 text=' Editor Preferences')
Tal Einat604e7b92018-09-25 15:10:14 +03001890 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
1891 text=' Shell Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001892 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
Tal Einat604e7b92018-09-25 15:10:14 +03001893 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001894 # Frame_window.
1895 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001896 startup_title = Label(frame_run, text='At Startup')
1897 self.startup_editor_on = Radiobutton(
1898 frame_run, variable=self.startup_edit, value=1,
1899 text="Open Edit Window")
1900 self.startup_shell_on = Radiobutton(
1901 frame_run, variable=self.startup_edit, value=0,
1902 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001903
wohlganger58fc71c2017-09-10 16:19:47 -05001904 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001905 win_size_title = Label(
1906 frame_win_size, text='Initial Window Size (in characters)')
1907 win_width_title = Label(frame_win_size, text='Width')
1908 self.win_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001909 frame_win_size, textvariable=self.win_width, width=3,
1910 validatecommand=self.digits_only, validate='key',
1911 )
csabellae8eb17b2017-07-30 18:39:17 -04001912 win_height_title = Label(frame_win_size, text='Height')
1913 self.win_height_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001914 frame_win_size, textvariable=self.win_height, width=3,
1915 validatecommand=self.digits_only, validate='key',
1916 )
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001917
wohlganger58fc71c2017-09-10 16:19:47 -05001918 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1919 auto_wait_title = Label(frame_autocomplete,
1920 text='Completions Popup Wait (milliseconds)')
1921 self.auto_wait_int = Entry(frame_autocomplete, width=6,
Tal Einat1ebee372019-07-23 13:02:11 +03001922 textvariable=self.autocomplete_wait,
1923 validatecommand=self.digits_only,
1924 validate='key',
1925 )
wohlganger58fc71c2017-09-10 16:19:47 -05001926
1927 frame_paren1 = Frame(frame_window, borderwidth=0)
1928 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1929 self.paren_style_type = OptionMenu(
1930 frame_paren1, self.paren_style, 'expression',
1931 "opener","parens","expression")
1932 frame_paren2 = Frame(frame_window, borderwidth=0)
1933 paren_time_title = Label(
1934 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1935 '(0 is until next input)')
1936 self.paren_flash_time = Entry(
1937 frame_paren2, textvariable=self.flash_delay, width=6)
1938 self.bell_on = Checkbutton(
1939 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1940
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001941 # Frame_editor.
1942 frame_save = Frame(frame_editor, borderwidth=0)
1943 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1944 self.save_ask_on = Radiobutton(
1945 frame_save, variable=self.autosave, value=0,
1946 text="Prompt to Save")
1947 self.save_auto_on = Radiobutton(
1948 frame_save, variable=self.autosave, value=1,
1949 text='No Prompt')
1950
wohlganger58fc71c2017-09-10 16:19:47 -05001951 frame_format = Frame(frame_editor, borderwidth=0)
1952 format_width_title = Label(frame_format,
1953 text='Format Paragraph Max Width')
1954 self.format_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001955 frame_format, textvariable=self.format_width, width=4,
1956 validatecommand=self.digits_only, validate='key',
1957 )
wohlganger58fc71c2017-09-10 16:19:47 -05001958
Tal Einat7123ea02019-07-23 15:22:11 +03001959 frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
1960 line_numbers_default_title = Label(
1961 frame_line_numbers_default, text='Show line numbers in new windows')
1962 self.line_numbers_default_bool = Checkbutton(
1963 frame_line_numbers_default,
1964 variable=self.line_numbers_default,
1965 width=1)
1966
wohlganger58fc71c2017-09-10 16:19:47 -05001967 frame_context = Frame(frame_editor, borderwidth=0)
Cheryl Sabella29996a12018-06-01 19:23:00 -04001968 context_title = Label(frame_context, text='Max Context Lines :')
wohlganger58fc71c2017-09-10 16:19:47 -05001969 self.context_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001970 frame_context, textvariable=self.context_lines, width=3,
1971 validatecommand=self.digits_only, validate='key',
1972 )
wohlganger58fc71c2017-09-10 16:19:47 -05001973
Tal Einat604e7b92018-09-25 15:10:14 +03001974 # Frame_shell.
1975 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
1976 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
1977 text='Auto-Squeeze Min. Lines:')
1978 self.auto_squeeze_min_lines_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001979 frame_auto_squeeze_min_lines, width=4,
1980 textvariable=self.auto_squeeze_min_lines,
1981 validatecommand=self.digits_only, validate='key',
1982 )
wohlganger58fc71c2017-09-10 16:19:47 -05001983
csabellae8eb17b2017-07-30 18:39:17 -04001984 # frame_help.
1985 frame_helplist = Frame(frame_help)
1986 frame_helplist_buttons = Frame(frame_helplist)
1987 self.helplist = Listbox(
1988 frame_helplist, height=5, takefocus=True,
1989 exportselection=FALSE)
1990 scroll_helplist = Scrollbar(frame_helplist)
1991 scroll_helplist['command'] = self.helplist.yview
1992 self.helplist['yscrollcommand'] = scroll_helplist.set
1993 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1994 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001995 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001996 width=8, command=self.helplist_item_edit)
1997 self.button_helplist_add = Button(
1998 frame_helplist_buttons, text='Add',
1999 width=8, command=self.helplist_item_add)
2000 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04002001 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04002002 width=8, command=self.helplist_item_remove)
2003
2004 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002005 # Body.
2006 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2007 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Tal Einat604e7b92018-09-25 15:10:14 +03002008 frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04002009 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2010 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002011 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002012 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2013 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2014 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04002015 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002016 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002017 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2018 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2019 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
2020 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2021 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002022 # frame_autocomplete.
2023 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
2024 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2025 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
2026 # frame_paren.
2027 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
2028 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2029 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
2030 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
2031 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
2032 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
2033 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
2034
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002035 # frame_save.
2036 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
2037 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2038 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2039 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002040 # frame_format.
2041 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
2042 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2043 self.format_width_int.pack(side=TOP, padx=10, pady=5)
Tal Einat7123ea02019-07-23 15:22:11 +03002044 # frame_line_numbers_default.
2045 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
2046 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2047 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002048 # frame_context.
2049 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
2050 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2051 self.context_int.pack(side=TOP, padx=5, pady=5)
2052
Tal Einat604e7b92018-09-25 15:10:14 +03002053 # frame_auto_squeeze_min_lines
2054 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
2055 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2056 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
2057
csabellae8eb17b2017-07-30 18:39:17 -04002058 # frame_help.
2059 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
2060 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2061 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
2062 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
2063 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
2064 self.button_helplist_add.pack(side=TOP, anchor=W)
2065 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
2066
2067 def load_general_cfg(self):
2068 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05002069 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04002070 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05002071 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04002072 self.win_width.set(idleConf.GetOption(
2073 'main', 'EditorWindow', 'width', type='int'))
2074 self.win_height.set(idleConf.GetOption(
2075 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002076 self.autocomplete_wait.set(idleConf.GetOption(
2077 'extensions', 'AutoComplete', 'popupwait', type='int'))
2078 self.paren_style.set(idleConf.GetOption(
2079 'extensions', 'ParenMatch', 'style'))
2080 self.flash_delay.set(idleConf.GetOption(
2081 'extensions', 'ParenMatch', 'flash-delay', type='int'))
2082 self.paren_bell.set(idleConf.GetOption(
2083 'extensions', 'ParenMatch', 'bell'))
2084
2085 # Set variables for editor windows.
2086 self.autosave.set(idleConf.GetOption(
2087 'main', 'General', 'autosave', default=0, type='bool'))
2088 self.format_width.set(idleConf.GetOption(
2089 'extensions', 'FormatParagraph', 'max-width', type='int'))
Tal Einat7123ea02019-07-23 15:22:11 +03002090 self.line_numbers_default.set(idleConf.GetOption(
2091 'main', 'EditorWindow', 'line-numbers-default', type='bool'))
wohlganger58fc71c2017-09-10 16:19:47 -05002092 self.context_lines.set(idleConf.GetOption(
Cheryl Sabella29996a12018-06-01 19:23:00 -04002093 'extensions', 'CodeContext', 'maxlines', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002094
Tal Einat604e7b92018-09-25 15:10:14 +03002095 # Set variables for shell windows.
2096 self.auto_squeeze_min_lines.set(idleConf.GetOption(
2097 'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
2098
csabellae8eb17b2017-07-30 18:39:17 -04002099 # Set additional help sources.
2100 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2101 self.helplist.delete(0, 'end')
2102 for help_item in self.user_helplist:
2103 self.helplist.insert(END, help_item[0])
2104 self.set_add_delete_state()
2105
2106 def help_source_selected(self, event):
2107 "Handle event for selecting additional help."
2108 self.set_add_delete_state()
2109
2110 def set_add_delete_state(self):
2111 "Toggle the state for the help list buttons based on list entries."
2112 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002113 self.button_helplist_edit.state(('disabled',))
2114 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002115 else: # Some entries.
2116 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002117 self.button_helplist_edit.state(('!disabled',))
2118 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002119 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002120 self.button_helplist_edit.state(('disabled',))
2121 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002122
2123 def helplist_item_add(self):
2124 """Handle add button for the help list.
2125
2126 Query for name and location of new help sources and add
2127 them to the list.
2128 """
2129 help_source = HelpSource(self, 'New Help Source').result
2130 if help_source:
2131 self.user_helplist.append(help_source)
2132 self.helplist.insert(END, help_source[0])
2133 self.update_help_changes()
2134
2135 def helplist_item_edit(self):
2136 """Handle edit button for the help list.
2137
2138 Query with existing help source information and update
2139 config if the values are changed.
2140 """
2141 item_index = self.helplist.index(ANCHOR)
2142 help_source = self.user_helplist[item_index]
2143 new_help_source = HelpSource(
2144 self, 'Edit Help Source',
2145 menuitem=help_source[0],
2146 filepath=help_source[1],
2147 ).result
2148 if new_help_source and new_help_source != help_source:
2149 self.user_helplist[item_index] = new_help_source
2150 self.helplist.delete(item_index)
2151 self.helplist.insert(item_index, new_help_source[0])
2152 self.update_help_changes()
2153 self.set_add_delete_state() # Selected will be un-selected
2154
2155 def helplist_item_remove(self):
2156 """Handle remove button for the help list.
2157
2158 Delete the help list item from config.
2159 """
2160 item_index = self.helplist.index(ANCHOR)
2161 del(self.user_helplist[item_index])
2162 self.helplist.delete(item_index)
2163 self.update_help_changes()
2164 self.set_add_delete_state()
2165
2166 def update_help_changes(self):
2167 "Clear and rebuild the HelpFiles section in changes"
2168 changes['main']['HelpFiles'] = {}
2169 for num in range(1, len(self.user_helplist) + 1):
2170 changes.add_option(
2171 'main', 'HelpFiles', str(num),
2172 ';'.join(self.user_helplist[num-1][:2]))
2173
2174
csabella45bf7232017-07-26 19:09:58 -04002175class VarTrace:
2176 """Maintain Tk variables trace state."""
2177
2178 def __init__(self):
2179 """Store Tk variables and callbacks.
2180
2181 untraced: List of tuples (var, callback)
2182 that do not have the callback attached
2183 to the Tk var.
2184 traced: List of tuples (var, callback) where
2185 that callback has been attached to the var.
2186 """
2187 self.untraced = []
2188 self.traced = []
2189
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002190 def clear(self):
2191 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002192 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002193 self.untraced.clear()
2194 self.traced.clear()
2195
csabella45bf7232017-07-26 19:09:58 -04002196 def add(self, var, callback):
2197 """Add (var, callback) tuple to untraced list.
2198
2199 Args:
2200 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002201 callback: Either function name to be used as a callback
2202 or a tuple with IdleConf config-type, section, and
2203 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002204
2205 Return:
2206 Tk variable instance.
2207 """
2208 if isinstance(callback, tuple):
2209 callback = self.make_callback(var, callback)
2210 self.untraced.append((var, callback))
2211 return var
2212
2213 @staticmethod
2214 def make_callback(var, config):
2215 "Return default callback function to add values to changes instance."
2216 def default_callback(*params):
2217 "Add config values to changes instance."
2218 changes.add_option(*config, var.get())
2219 return default_callback
2220
2221 def attach(self):
2222 "Attach callback to all vars that are not traced."
2223 while self.untraced:
2224 var, callback = self.untraced.pop()
2225 var.trace_add('write', callback)
2226 self.traced.append((var, callback))
2227
2228 def detach(self):
2229 "Remove callback from traced vars."
2230 while self.traced:
2231 var, callback = self.traced.pop()
2232 var.trace_remove('write', var.trace_info()[0][1])
2233 self.untraced.append((var, callback))
2234
2235
csabella5b591542017-07-28 14:40:59 -04002236tracers = VarTrace()
2237
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002238help_common = '''\
2239When you click either the Apply or Ok buttons, settings in this
2240dialog that are different from IDLE's default are saved in
2241a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002242these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002243machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002244'''
2245help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002246 'Fonts/Tabs':'''
2247Font sample: This shows what a selection of Basic Multilingual Plane
2248unicode characters look like for the current font selection. If the
2249selected font does not define a character, Tk attempts to find another
2250font that does. Substitute glyphs depend on what is available on a
2251particular system and will not necessarily have the same size as the
2252font selected. Line contains 20 characters up to Devanagari, 14 for
2253Tamil, and 10 for East Asia.
2254
2255Hebrew and Arabic letters should display right to left, starting with
2256alef, \u05d0 and \u0627. Arabic digits display left to right. The
2257Devanagari and Tamil lines start with digits. The East Asian lines
2258are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2259Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002260
2261You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002262''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002263 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002264Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002265The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002266be used with older IDLE releases if it is saved as a custom
2267theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002268''',
2269 'Keys': '''
2270Keys:
2271The IDLE Modern Unix key set is new in June 2016. It can only
2272be used with older IDLE releases if it is saved as a custom
2273key set, with a different name.
2274''',
wohlganger58fc71c2017-09-10 16:19:47 -05002275 'General': '''
2276General:
wohlgangerfae2c352017-06-27 21:36:23 -05002277
penguindustin96466302019-05-06 14:57:17 -04002278AutoComplete: Popupwait is milliseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002279cursor movement, before popping up completion box. Key char is '.' after
2280identifier or a '/' (or '\\' on Windows) within a string.
2281
2282FormatParagraph: Max-width is max chars in lines after re-formatting.
2283Use with paragraphs in both strings and comment blocks.
2284
2285ParenMatch: Style indicates what is highlighted when closer is entered:
2286'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2287'expression' (default) - also everything in between. Flash-delay is how
2288long to highlight if cursor is not moved (0 means forever).
Cheryl Sabella29996a12018-06-01 19:23:00 -04002289
2290CodeContext: Maxlines is the maximum number of code context lines to
2291display when Code Context is turned on for an editor window.
Tal Einat604e7b92018-09-25 15:10:14 +03002292
2293Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
2294of output to automatically "squeeze".
wohlgangerfae2c352017-06-27 21:36:23 -05002295'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002296}
2297
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002298
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002299def is_int(s):
2300 "Return 's is blank or represents an int'"
2301 if not s:
2302 return True
2303 try:
2304 int(s)
2305 return True
2306 except ValueError:
2307 return False
2308
2309
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002310class VerticalScrolledFrame(Frame):
2311 """A pure Tkinter vertically scrollable frame.
2312
2313 * Use the 'interior' attribute to place widgets inside the scrollable frame
2314 * Construct and pack/place/grid normally
2315 * This frame only allows vertical scrolling
2316 """
2317 def __init__(self, parent, *args, **kw):
2318 Frame.__init__(self, parent, *args, **kw)
2319
csabella7eb58832017-07-04 21:30:58 -04002320 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002321 vscrollbar = Scrollbar(self, orient=VERTICAL)
2322 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002323 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002324 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002325 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2326 vscrollbar.config(command=canvas.yview)
2327
csabella7eb58832017-07-04 21:30:58 -04002328 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002329 canvas.xview_moveto(0)
2330 canvas.yview_moveto(0)
2331
csabella7eb58832017-07-04 21:30:58 -04002332 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002333 self.interior = interior = Frame(canvas)
2334 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2335
csabella7eb58832017-07-04 21:30:58 -04002336 # Track changes to the canvas and frame width and sync them,
2337 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002338 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002339 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002340 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2341 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002342 interior.bind('<Configure>', _configure_interior)
2343
2344 def _configure_canvas(event):
2345 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002346 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002347 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2348 canvas.bind('<Configure>', _configure_canvas)
2349
2350 return
2351
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002352
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002353if __name__ == '__main__':
Terry Jan Reedy4d921582018-06-19 19:12:52 -04002354 from unittest import main
2355 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
2356
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002357 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002358 run(ConfigDialog)