blob: 22359735874d19f417ba1b66e094cb8fc009ef88 [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
Tal Einat3221a632019-07-27 19:57:48 +030036from idlelib.textview import ScrollableTextFrame
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -040037
terryjreedy349abd92017-07-07 16:00:57 -040038changes = ConfigChanges()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040039# Reload changed options in the following classes.
Tal Einat604e7b92018-09-25 15:10:14 +030040reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
41 Squeezer)
terryjreedy349abd92017-07-07 16:00:57 -040042
csabella5b591542017-07-28 14:40:59 -040043
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000044class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -040045 """Config dialog for IDLE.
46 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000047
Terry Jan Reedybfebfd82017-09-30 17:37:53 -040048 def __init__(self, parent, title='', *, _htest=False, _utest=False):
csabella7eb58832017-07-04 21:30:58 -040049 """Show the tabbed dialog for user configuration.
50
csabella36329a42017-07-13 23:32:01 -040051 Args:
52 parent - parent of this dialog
53 title - string which is the title of this popup dialog
54 _htest - bool, change box location when running htest
55 _utest - bool, don't wait_window when running unittest
56
57 Note: Focus set on font page fontlist.
58
59 Methods:
60 create_widgets
61 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040062 """
Steven M. Gavad721c482001-07-31 10:46:53 +000063 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040064 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040065 if _htest:
66 parent.instance_dict = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080067 if not _utest:
68 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000069
Steven M. Gavad721c482001-07-31 10:46:53 +000070 self.configure(borderwidth=5)
Terry Jan Reedycd567362014-10-17 01:31:35 -040071 self.title(title or 'IDLE Preferences')
csabellabac7d332017-06-26 17:46:26 -040072 x = parent.winfo_rootx() + 20
73 y = parent.winfo_rooty() + (30 if not _htest else 150)
74 self.geometry(f'+{x}+{y}')
csabella7eb58832017-07-04 21:30:58 -040075 # Each theme element key is its display name.
76 # The first value of the tuple is the sample area tag name.
77 # The second value is the display name list sort index.
csabellabac7d332017-06-26 17:46:26 -040078 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040079 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000080 self.transient(parent)
csabellabac7d332017-06-26 17:46:26 -040081 self.protocol("WM_DELETE_WINDOW", self.cancel)
csabella9397e2a2017-07-30 13:34:25 -040082 self.fontpage.fontlist.focus_set()
csabella7eb58832017-07-04 21:30:58 -040083 # XXX Decide whether to keep or delete these key bindings.
84 # Key bindings for this dialog.
85 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
86 # self.bind('<Alt-a>', self.Apply) #apply changes, save
87 # self.bind('<F1>', self.Help) #context help
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040088 # Attach callbacks after loading config to avoid calling them.
csabella5b591542017-07-28 14:40:59 -040089 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000090
Terry Jan Reedycfa89502014-07-14 23:07:32 -040091 if not _utest:
Louie Lu9b622fb2017-07-14 08:35:48 +080092 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040093 self.wm_deiconify()
94 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000095
csabellabac7d332017-06-26 17:46:26 -040096 def create_widgets(self):
csabella36329a42017-07-13 23:32:01 -040097 """Create and place widgets for tabbed dialog.
98
99 Widgets Bound to self:
csabellae8eb17b2017-07-30 18:39:17 -0400100 note: Notebook
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400101 highpage: HighPage
csabellae8eb17b2017-07-30 18:39:17 -0400102 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400103 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -0400104 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400105 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -0400106
107 Methods:
csabella36329a42017-07-13 23:32:01 -0400108 create_action_buttons
109 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400110 activate_config_changes: Tell editors to reload.
111 """
Terry Jan Reedyd6e2f262017-09-19 19:01:45 -0400112 self.note = note = Notebook(self)
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400113 self.highpage = HighPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400114 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400115 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400116 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400117 self.extpage = self.create_page_extensions()
118 note.add(self.fontpage, text='Fonts/Tabs')
119 note.add(self.highpage, text='Highlights')
120 note.add(self.keyspage, text=' Keys ')
121 note.add(self.genpage, text=' General ')
122 note.add(self.extpage, text='Extensions')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400123 note.enable_traversal()
124 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400125 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400126
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400127 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -0400128 """Return frame of action buttons for dialog.
129
130 Methods:
131 ok
132 apply
133 cancel
134 help
135
136 Widget Structure:
137 outer: Frame
138 buttons: Frame
139 (no assignment): Button (ok)
140 (no assignment): Button (apply)
141 (no assignment): Button (cancel)
142 (no assignment): Button (help)
143 (no assignment): Frame
144 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400145 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400146 # Changing the default padding on OSX results in unreadable
csabella7eb58832017-07-04 21:30:58 -0400147 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400148 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000149 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400150 padding_args = {'padding': (6, 3)}
151 outer = Frame(self, padding=2)
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500152 buttons_frame = Frame(outer, padding=2)
153 self.buttons = {}
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400154 for txt, cmd in (
csabellabac7d332017-06-26 17:46:26 -0400155 ('Ok', self.ok),
156 ('Apply', self.apply),
157 ('Cancel', self.cancel),
158 ('Help', self.help)):
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500159 self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd,
160 takefocus=FALSE, **padding_args)
161 self.buttons[txt].pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -0400162 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400163 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500164 buttons_frame.pack(side=BOTTOM)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400165 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400166
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400167 def ok(self):
168 """Apply config changes, then dismiss dialog.
169
170 Methods:
171 apply
172 destroy: inherited
173 """
174 self.apply()
175 self.destroy()
176
177 def apply(self):
178 """Apply config changes and leave dialog open.
179
180 Methods:
181 deactivate_current_config
182 save_all_changed_extensions
183 activate_config_changes
184 """
185 self.deactivate_current_config()
186 changes.save_all()
187 self.save_all_changed_extensions()
188 self.activate_config_changes()
189
190 def cancel(self):
191 """Dismiss config dialog.
192
193 Methods:
194 destroy: inherited
195 """
Cheryl Sabellad0d9fa82020-01-25 04:00:54 -0500196 changes.clear()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400197 self.destroy()
198
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300199 def destroy(self):
200 global font_sample_text
201 font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
Tal Einat10ea9402018-08-02 09:18:29 +0300202 self.grab_release()
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300203 super().destroy()
204
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400205 def help(self):
206 """Create textview for config dialog help.
207
Xtreakd9677f32019-06-03 09:51:15 +0530208 Attributes accessed:
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400209 note
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400210 Methods:
211 view_text: Method from textview module.
212 """
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400213 page = self.note.tab(self.note.select(), option='text').strip()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400214 view_text(self, title='Help for IDLE preferences',
Zackery Spytz2e43b642020-01-22 20:54:30 -0700215 contents=help_common+help_pages.get(page, ''))
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400216
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400217 def deactivate_current_config(self):
218 """Remove current key bindings.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400219 Iterate over window instances defined in parent and remove
220 the keybindings.
221 """
222 # Before a config is saved, some cleanup of current
223 # config must be done - remove the previous keybindings.
224 win_instances = self.parent.instance_dict.keys()
225 for instance in win_instances:
226 instance.RemoveKeybindings()
227
228 def activate_config_changes(self):
229 """Apply configuration changes to current windows.
230
231 Dynamically update the current parent window instances
232 with some of the configuration changes.
233 """
234 win_instances = self.parent.instance_dict.keys()
235 for instance in win_instances:
236 instance.ResetColorizer()
237 instance.ResetFont()
238 instance.set_notabs_indentwidth()
239 instance.ApplyKeybindings()
240 instance.reset_help_menu_entries()
Zackery Spytz9c284492019-11-13 00:13:33 -0700241 instance.update_cursor_blink()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -0400242 for klass in reloadables:
243 klass.reload()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400244
csabellabac7d332017-06-26 17:46:26 -0400245 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400246 """Part of the config dialog used for configuring IDLE extensions.
247
248 This code is generic - it works for any and all IDLE extensions.
249
250 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400251 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400252 GUI interface to change the configuration values, and saves the
253 changes using idleConf.
254
255 Not all changes take effect immediately - some may require restarting IDLE.
256 This depends on each extension's implementation.
257
258 All values are treated as text, and it is up to the user to supply
259 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300260 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -0400261
262 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300263 load_extensions:
csabella36329a42017-07-13 23:32:01 -0400264 extension_selected: Handle selection from list.
265 create_extension_frame: Hold widgets for one extension.
266 set_extension_value: Set in userCfg['extensions'].
267 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400268 """
269 parent = self.parent
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400270 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400271 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
272 self.ext_userCfg = idleConf.userCfg['extensions']
273 self.is_int = self.register(is_int)
274 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -0400275 # Create widgets - a listbox shows all available extensions, with the
276 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400277 self.extension_names = StringVar(self)
278 frame.rowconfigure(0, weight=1)
279 frame.columnconfigure(2, weight=1)
280 self.extension_list = Listbox(frame, listvariable=self.extension_names,
281 selectmode='browse')
282 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
283 scroll = Scrollbar(frame, command=self.extension_list.yview)
284 self.extension_list.yscrollcommand=scroll.set
285 self.details_frame = LabelFrame(frame, width=250, height=250)
286 self.extension_list.grid(column=0, row=0, sticky='nws')
287 scroll.grid(column=1, row=0, sticky='ns')
288 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
Cheryl Sabella7028e592017-08-26 14:26:02 -0400289 frame.configure(padding=10)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400290 self.config_frame = {}
291 self.current_extension = None
292
293 self.outerframe = self # TEMPORARY
294 self.tabbed_page_set = self.extension_list # TEMPORARY
295
csabella7eb58832017-07-04 21:30:58 -0400296 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400297 ext_names = ''
298 for ext_name in sorted(self.extensions):
299 self.create_extension_frame(ext_name)
300 ext_names = ext_names + '{' + ext_name + '} '
301 self.extension_names.set(ext_names)
302 self.extension_list.selection_set(0)
303 self.extension_selected(None)
304
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400305 return frame
306
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400307 def load_extensions(self):
308 "Fill self.extensions with data from the default and user configs."
309 self.extensions = {}
310 for ext_name in idleConf.GetExtensions(active_only=False):
wohlganger58fc71c2017-09-10 16:19:47 -0500311 # Former built-in extensions are already filtered out.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400312 self.extensions[ext_name] = []
313
314 for ext_name in self.extensions:
315 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
316
csabella7eb58832017-07-04 21:30:58 -0400317 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400318 enables = [opt_name for opt_name in opt_list
319 if opt_name.startswith('enable')]
320 for opt_name in enables:
321 opt_list.remove(opt_name)
322 opt_list = enables + opt_list
323
324 for opt_name in opt_list:
325 def_str = self.ext_defaultCfg.Get(
326 ext_name, opt_name, raw=True)
327 try:
328 def_obj = {'True':True, 'False':False}[def_str]
329 opt_type = 'bool'
330 except KeyError:
331 try:
332 def_obj = int(def_str)
333 opt_type = 'int'
334 except ValueError:
335 def_obj = def_str
336 opt_type = None
337 try:
338 value = self.ext_userCfg.Get(
339 ext_name, opt_name, type=opt_type, raw=True,
340 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -0400341 except ValueError: # Need this until .Get fixed.
342 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400343 var = StringVar(self)
344 var.set(str(value))
345
346 self.extensions[ext_name].append({'name': opt_name,
347 'type': opt_type,
348 'default': def_str,
349 'value': value,
350 'var': var,
351 })
352
353 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -0400354 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400355 newsel = self.extension_list.curselection()
356 if newsel:
357 newsel = self.extension_list.get(newsel)
358 if newsel is None or newsel != self.current_extension:
359 if self.current_extension:
360 self.details_frame.config(text='')
361 self.config_frame[self.current_extension].grid_forget()
362 self.current_extension = None
363 if newsel:
364 self.details_frame.config(text=newsel)
365 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
366 self.current_extension = newsel
367
368 def create_extension_frame(self, ext_name):
369 """Create a frame holding the widgets to configure one extension"""
370 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
371 self.config_frame[ext_name] = f
372 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -0400373 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400374 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -0400375 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400376 label = Label(entry_area, text=opt['name'])
377 label.grid(row=row, column=0, sticky=NW)
378 var = opt['var']
379 if opt['type'] == 'bool':
Cheryl Sabella7028e592017-08-26 14:26:02 -0400380 Checkbutton(entry_area, variable=var,
381 onvalue='True', offvalue='False', width=8
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400382 ).grid(row=row, column=1, sticky=W, padx=7)
383 elif opt['type'] == 'int':
384 Entry(entry_area, textvariable=var, validate='key',
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400385 validatecommand=(self.is_int, '%P'), width=10
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400386 ).grid(row=row, column=1, sticky=NSEW, padx=7)
387
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400388 else: # type == 'str'
389 # Limit size to fit non-expanding space with larger font.
390 Entry(entry_area, textvariable=var, width=15
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400391 ).grid(row=row, column=1, sticky=NSEW, padx=7)
392 return
393
394 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400395 """Return True if the configuration was added or changed.
396
397 If the value is the same as the default, then remove it
398 from user config file.
399 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400400 name = opt['name']
401 default = opt['default']
402 value = opt['var'].get().strip() or default
403 opt['var'].set(value)
404 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400405 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400406 if (value == default):
407 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400408 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400409 return self.ext_userCfg.SetOption(section, name, value)
410
411 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400412 """Save configuration changes to the user config file.
413
414 Attributes accessed:
415 extensions
416
417 Methods:
418 set_extension_value
419 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400420 has_changes = False
421 for ext_name in self.extensions:
422 options = self.extensions[ext_name]
423 for opt in options:
424 if self.set_extension_value(ext_name, opt):
425 has_changes = True
426 if has_changes:
427 self.ext_userCfg.Save()
428
429
csabella6f446be2017-08-01 00:24:07 -0400430# class TabPage(Frame): # A template for Page classes.
431# def __init__(self, master):
432# super().__init__(master)
433# self.create_page_tab()
434# self.load_tab_cfg()
435# def create_page_tab(self):
436# # Define tk vars and register var and callback with tracers.
437# # Create subframes and widgets.
438# # Pack widgets.
439# def load_tab_cfg(self):
440# # Initialize widgets with data from idleConf.
441# def var_changed_var_name():
442# # For each tk var that needs other than default callback.
443# def other_methods():
444# # Define tab-specific behavior.
445
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300446font_sample_text = (
447 '<ASCII/Latin1>\n'
448 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
449 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
450 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
451 '\n<IPA,Greek,Cyrillic>\n'
452 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
453 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
454 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
455 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
456 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
457 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
458 '\n<Hebrew, Arabic>\n'
459 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
460 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
461 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
462 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
463 '\n<Devanagari, Tamil>\n'
464 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
465 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
466 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
467 '\u0b85\u0b87\u0b89\u0b8e\n'
468 '\n<East Asian>\n'
469 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
470 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
471 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
472 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
473 )
474
csabella6f446be2017-08-01 00:24:07 -0400475
csabella9397e2a2017-07-30 13:34:25 -0400476class FontPage(Frame):
477
csabella6f446be2017-08-01 00:24:07 -0400478 def __init__(self, master, highpage):
479 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400480 self.highlight_sample = highpage.highlight_sample
481 self.create_page_font_tab()
482 self.load_font_cfg()
483 self.load_tab_cfg()
484
485 def create_page_font_tab(self):
486 """Return frame of widgets for Font/Tabs tab.
487
488 Fonts: Enable users to provisionally change font face, size, or
489 boldness and to see the consequence of proposed choices. Each
490 action set 3 options in changes structuree and changes the
491 corresponding aspect of the font sample on this page and
492 highlight sample on highlight page.
493
494 Function load_font_cfg initializes font vars and widgets from
495 idleConf entries and tk.
496
497 Fontlist: mouse button 1 click or up or down key invoke
498 on_fontlist_select(), which sets var font_name.
499
500 Sizelist: clicking the menubutton opens the dropdown menu. A
501 mouse button 1 click or return key sets var font_size.
502
503 Bold_toggle: clicking the box toggles var font_bold.
504
505 Changing any of the font vars invokes var_changed_font, which
506 adds all 3 font options to changes and calls set_samples.
507 Set_samples applies a new font constructed from the font vars to
Leo Ariasc3d95082018-02-03 18:36:10 -0600508 font_sample and to highlight_sample on the highlight page.
csabella9397e2a2017-07-30 13:34:25 -0400509
510 Tabs: Enable users to change spaces entered for indent tabs.
511 Changing indent_scale value with the mouse sets Var space_num,
512 which invokes the default callback to add an entry to
513 changes. Load_tab_cfg initializes space_num to default.
514
Cheryl Sabella2f896462017-08-14 21:21:43 -0400515 Widgets for FontPage(Frame): (*) widgets bound to self
516 frame_font: LabelFrame
517 frame_font_name: Frame
518 font_name_title: Label
519 (*)fontlist: ListBox - font_name
520 scroll_font: Scrollbar
521 frame_font_param: Frame
522 font_size_title: Label
523 (*)sizelist: DynOptionMenu - font_size
524 (*)bold_toggle: Checkbutton - font_bold
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400525 frame_sample: LabelFrame
526 (*)font_sample: Label
Cheryl Sabella2f896462017-08-14 21:21:43 -0400527 frame_indent: LabelFrame
528 indent_title: Label
529 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400530 """
csabella6f446be2017-08-01 00:24:07 -0400531 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
532 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
533 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400534 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
535
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400536 # Define frames and widgets.
csabella9397e2a2017-07-30 13:34:25 -0400537 frame_font = LabelFrame(
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400538 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
539 frame_sample = LabelFrame(
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300540 self, borderwidth=2, relief=GROOVE,
541 text=' Font Sample (Editable) ')
csabella9397e2a2017-07-30 13:34:25 -0400542 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400543 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400544 # frame_font.
545 frame_font_name = Frame(frame_font)
546 frame_font_param = Frame(frame_font)
547 font_name_title = Label(
548 frame_font_name, justify=LEFT, text='Font Face :')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400549 self.fontlist = Listbox(frame_font_name, height=15,
csabella9397e2a2017-07-30 13:34:25 -0400550 takefocus=True, exportselection=FALSE)
551 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
552 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
553 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
554 scroll_font = Scrollbar(frame_font_name)
555 scroll_font.config(command=self.fontlist.yview)
556 self.fontlist.config(yscrollcommand=scroll_font.set)
557 font_size_title = Label(frame_font_param, text='Size :')
558 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
559 self.bold_toggle = Checkbutton(
560 frame_font_param, variable=self.font_bold,
561 onvalue=1, offvalue=0, text='Bold')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400562 # frame_sample.
Tal Einat3221a632019-07-27 19:57:48 +0300563 font_sample_frame = ScrollableTextFrame(frame_sample)
564 self.font_sample = font_sample_frame.text
565 self.font_sample.config(wrap=NONE, width=1, height=1)
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300566 self.font_sample.insert(END, font_sample_text)
csabella9397e2a2017-07-30 13:34:25 -0400567 # frame_indent.
568 indent_title = Label(
569 frame_indent, justify=LEFT,
570 text='Python Standard: 4 Spaces!')
571 self.indent_scale = Scale(
572 frame_indent, variable=self.space_num,
573 orient='horizontal', tickinterval=2, from_=2, to=16)
574
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400575 # Grid and pack widgets:
576 self.columnconfigure(1, weight=1)
Tal Einat3221a632019-07-27 19:57:48 +0300577 self.rowconfigure(2, weight=1)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400578 frame_font.grid(row=0, column=0, padx=5, pady=5)
Tal Einat3221a632019-07-27 19:57:48 +0300579 frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5,
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400580 sticky='nsew')
581 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
csabella9397e2a2017-07-30 13:34:25 -0400582 # frame_font.
583 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
584 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
585 font_name_title.pack(side=TOP, anchor=W)
586 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
587 scroll_font.pack(side=LEFT, fill=Y)
588 font_size_title.pack(side=LEFT, anchor=W)
589 self.sizelist.pack(side=LEFT, anchor=W)
590 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400591 # frame_sample.
Tal Einat3221a632019-07-27 19:57:48 +0300592 font_sample_frame.pack(expand=TRUE, fill=BOTH)
csabella9397e2a2017-07-30 13:34:25 -0400593 # frame_indent.
csabella9397e2a2017-07-30 13:34:25 -0400594 indent_title.pack(side=TOP, anchor=W, padx=5)
595 self.indent_scale.pack(side=TOP, padx=5, fill=X)
596
csabella9397e2a2017-07-30 13:34:25 -0400597 def load_font_cfg(self):
598 """Load current configuration settings for the font options.
599
600 Retrieve current font with idleConf.GetFont and font families
601 from tk. Setup fontlist and set font_name. Setup sizelist,
602 which sets font_size. Set font_bold. Call set_samples.
603 """
604 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
605 font_name = configured_font[0].lower()
606 font_size = configured_font[1]
607 font_bold = configured_font[2]=='bold'
608
609 # Set editor font selection list and font_name.
610 fonts = list(tkFont.families(self))
611 fonts.sort()
612 for font in fonts:
613 self.fontlist.insert(END, font)
614 self.font_name.set(font_name)
615 lc_fonts = [s.lower() for s in fonts]
616 try:
617 current_font_index = lc_fonts.index(font_name)
618 self.fontlist.see(current_font_index)
619 self.fontlist.select_set(current_font_index)
620 self.fontlist.select_anchor(current_font_index)
621 self.fontlist.activate(current_font_index)
622 except ValueError:
623 pass
624 # Set font size dropdown.
625 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
626 '16', '18', '20', '22', '25', '29', '34', '40'),
627 font_size)
628 # Set font weight.
629 self.font_bold.set(font_bold)
630 self.set_samples()
631
632 def var_changed_font(self, *params):
633 """Store changes to font attributes.
634
635 When one font attribute changes, save them all, as they are
636 not independent from each other. In particular, when we are
637 overriding the default font, we need to write out everything.
638 """
639 value = self.font_name.get()
640 changes.add_option('main', 'EditorWindow', 'font', value)
641 value = self.font_size.get()
642 changes.add_option('main', 'EditorWindow', 'font-size', value)
643 value = self.font_bold.get()
644 changes.add_option('main', 'EditorWindow', 'font-bold', value)
645 self.set_samples()
646
647 def on_fontlist_select(self, event):
648 """Handle selecting a font from the list.
649
650 Event can result from either mouse click or Up or Down key.
651 Set font_name and example displays to selection.
652 """
653 font = self.fontlist.get(
654 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
655 self.font_name.set(font.lower())
656
657 def set_samples(self, event=None):
658 """Update update both screen samples with the font settings.
659
660 Called on font initialization and change events.
661 Accesses font_name, font_size, and font_bold Variables.
Leo Ariasc3d95082018-02-03 18:36:10 -0600662 Updates font_sample and highlight page highlight_sample.
csabella9397e2a2017-07-30 13:34:25 -0400663 """
664 font_name = self.font_name.get()
665 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
666 new_font = (font_name, self.font_size.get(), font_weight)
667 self.font_sample['font'] = new_font
668 self.highlight_sample['font'] = new_font
669
670 def load_tab_cfg(self):
671 """Load current configuration settings for the tab options.
672
673 Attributes updated:
674 space_num: Set to value from idleConf.
675 """
676 # Set indent sizes.
677 space_num = idleConf.GetOption(
678 'main', 'Indent', 'num-spaces', default=4, type='int')
679 self.space_num.set(space_num)
680
681 def var_changed_space_num(self, *params):
682 "Store change to indentation size."
683 value = self.space_num.get()
684 changes.add_option('main', 'Indent', 'num-spaces', value)
685
686
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400687class HighPage(Frame):
688
689 def __init__(self, master):
690 super().__init__(master)
691 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400692 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400693 self.create_page_highlight()
694 self.load_theme_cfg()
695
696 def create_page_highlight(self):
697 """Return frame of widgets for Highlighting tab.
698
699 Enable users to provisionally change foreground and background
700 colors applied to textual tags. Color mappings are stored in
701 complete listings called themes. Built-in themes in
702 idlelib/config-highlight.def are fixed as far as the dialog is
703 concerned. Any theme can be used as the base for a new custom
704 theme, stored in .idlerc/config-highlight.cfg.
705
706 Function load_theme_cfg() initializes tk variables and theme
707 lists and calls paint_theme_sample() and set_highlight_target()
708 for the current theme. Radiobuttons builtin_theme_on and
709 custom_theme_on toggle var theme_source, which controls if the
710 current set of colors are from a builtin or custom theme.
711 DynOptionMenus builtinlist and customlist contain lists of the
712 builtin and custom themes, respectively, and the current item
713 from each list is stored in vars builtin_name and custom_name.
714
715 Function paint_theme_sample() applies the colors from the theme
716 to the tags in text widget highlight_sample and then invokes
717 set_color_sample(). Function set_highlight_target() sets the state
718 of the radiobuttons fg_on and bg_on based on the tag and it also
719 invokes set_color_sample().
720
721 Function set_color_sample() sets the background color for the frame
722 holding the color selector. This provides a larger visual of the
723 color for the current tag and plane (foreground/background).
724
725 Note: set_color_sample() is called from many places and is often
726 called more than once when a change is made. It is invoked when
727 foreground or background is selected (radiobuttons), from
728 paint_theme_sample() (theme is changed or load_cfg is called), and
729 from set_highlight_target() (target tag is changed or load_cfg called).
730
731 Button delete_custom invokes delete_custom() to delete
732 a custom theme from idleConf.userCfg['highlight'] and changes.
733 Button save_custom invokes save_as_new_theme() which calls
734 get_new_theme_name() and create_new() to save a custom theme
735 and its colors to idleConf.userCfg['highlight'].
736
737 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
738 if the current selected color for a tag is for the foreground or
739 background.
740
741 DynOptionMenu targetlist contains a readable description of the
742 tags applied to Python source within IDLE. Selecting one of the
743 tags from this list populates highlight_target, which has a callback
744 function set_highlight_target().
745
746 Text widget highlight_sample displays a block of text (which is
747 mock Python code) in which is embedded the defined tags and reflects
748 the color attributes of the current theme and changes for those tags.
749 Mouse button 1 allows for selection of a tag and updates
750 highlight_target with that tag value.
751
752 Note: The font in highlight_sample is set through the config in
753 the fonts tab.
754
755 In other words, a tag can be selected either from targetlist or
756 by clicking on the sample text within highlight_sample. The
757 plane (foreground/background) is selected via the radiobutton.
758 Together, these two (tag and plane) control what color is
759 shown in set_color_sample() for the current theme. Button set_color
760 invokes get_color() which displays a ColorChooser to change the
761 color for the selected tag/plane. If a new color is picked,
762 it will be saved to changes and the highlight_sample and
763 frame background will be updated.
764
765 Tk Variables:
766 color: Color of selected target.
767 builtin_name: Menu variable for built-in theme.
768 custom_name: Menu variable for custom theme.
769 fg_bg_toggle: Toggle for foreground/background color.
770 Note: this has no callback.
771 theme_source: Selector for built-in or custom theme.
772 highlight_target: Menu variable for the highlight tag target.
773
774 Instance Data Attributes:
775 theme_elements: Dictionary of tags for text highlighting.
776 The key is the display name and the value is a tuple of
777 (tag name, display sort order).
778
779 Methods [attachment]:
780 load_theme_cfg: Load current highlight colors.
781 get_color: Invoke colorchooser [button_set_color].
782 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
783 set_highlight_target: set fg_bg_toggle, set_color_sample().
784 set_color_sample: Set frame background to target.
785 on_new_color_set: Set new color and add option.
786 paint_theme_sample: Recolor sample.
787 get_new_theme_name: Get from popup.
788 create_new: Combine theme with changes and save.
789 save_as_new_theme: Save [button_save_custom].
790 set_theme_type: Command for [theme_source].
791 delete_custom: Activate default [button_delete_custom].
792 save_new: Save to userCfg['theme'] (is function).
793
794 Widgets of highlights page frame: (*) widgets bound to self
795 frame_custom: LabelFrame
796 (*)highlight_sample: Text
797 (*)frame_color_set: Frame
798 (*)button_set_color: Button
799 (*)targetlist: DynOptionMenu - highlight_target
800 frame_fg_bg_toggle: Frame
801 (*)fg_on: Radiobutton - fg_bg_toggle
802 (*)bg_on: Radiobutton - fg_bg_toggle
803 (*)button_save_custom: Button
804 frame_theme: LabelFrame
805 theme_type_title: Label
806 (*)builtin_theme_on: Radiobutton - theme_source
807 (*)custom_theme_on: Radiobutton - theme_source
808 (*)builtinlist: DynOptionMenu - builtin_name
809 (*)customlist: DynOptionMenu - custom_name
810 (*)button_delete_custom: Button
811 (*)theme_message: Label
812 """
813 self.theme_elements = {
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400814 'Normal Code or Text': ('normal', '00'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400815 'Code Context': ('context', '01'),
816 'Python Keywords': ('keyword', '02'),
817 'Python Definitions': ('definition', '03'),
818 'Python Builtins': ('builtin', '04'),
819 'Python Comments': ('comment', '05'),
820 'Python Strings': ('string', '06'),
821 'Selected Text': ('hilite', '07'),
822 'Found Text': ('hit', '08'),
823 'Cursor': ('cursor', '09'),
824 'Editor Breakpoint': ('break', '10'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400825 'Shell Prompt': ('console', '11'),
826 'Error Text': ('error', '12'),
827 'Shell User Output': ('stdout', '13'),
828 'Shell User Exception': ('stderr', '14'),
Tal Einat7123ea02019-07-23 15:22:11 +0300829 'Line Number': ('linenumber', '16'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400830 }
831 self.builtin_name = tracers.add(
832 StringVar(self), self.var_changed_builtin_name)
833 self.custom_name = tracers.add(
834 StringVar(self), self.var_changed_custom_name)
835 self.fg_bg_toggle = BooleanVar(self)
836 self.color = tracers.add(
837 StringVar(self), self.var_changed_color)
838 self.theme_source = tracers.add(
839 BooleanVar(self), self.var_changed_theme_source)
840 self.highlight_target = tracers.add(
841 StringVar(self), self.var_changed_highlight_target)
842
843 # Create widgets:
844 # body frame and section frames.
845 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
846 text=' Custom Highlighting ')
847 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
848 text=' Highlighting Theme ')
849 # frame_custom.
Tal Einat3221a632019-07-27 19:57:48 +0300850 sample_frame = ScrollableTextFrame(
851 frame_custom, relief=SOLID, borderwidth=1)
852 text = self.highlight_sample = sample_frame.text
853 text.configure(
854 font=('courier', 12, ''), cursor='hand2', width=1, height=1,
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400855 takefocus=FALSE, highlightthickness=0, wrap=NONE)
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500856 # Prevent perhaps invisible selection of word or slice.
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400857 text.bind('<Double-Button-1>', lambda e: 'break')
858 text.bind('<B1-Motion>', lambda e: 'break')
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400859 string_tags=(
860 ('# Click selects item.', 'comment'), ('\n', 'normal'),
861 ('code context section', 'context'), ('\n', 'normal'),
862 ('| cursor', 'cursor'), ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400863 ('def', 'keyword'), (' ', 'normal'),
864 ('func', 'definition'), ('(param):\n ', 'normal'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400865 ('"Return None."', 'string'), ('\n var0 = ', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400866 ("'string'", 'string'), ('\n var1 = ', 'normal'),
867 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
868 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
869 ('list', 'builtin'), ('(', 'normal'),
870 ('None', 'keyword'), (')\n', 'normal'),
871 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400872 ('>>>', 'console'), (' 3.14**2\n', 'normal'),
873 ('9.8596', 'stdout'), ('\n', 'normal'),
874 ('>>>', 'console'), (' pri ', 'normal'),
875 ('n', 'error'), ('t(\n', 'normal'),
876 ('SyntaxError', 'stderr'), ('\n', 'normal'))
877 for string, tag in string_tags:
878 text.insert(END, string, tag)
Tal Einat7123ea02019-07-23 15:22:11 +0300879 n_lines = len(text.get('1.0', END).splitlines())
Tal Einat3221a632019-07-27 19:57:48 +0300880 for lineno in range(1, n_lines):
Tal Einat7123ea02019-07-23 15:22:11 +0300881 text.insert(f'{lineno}.0',
882 f'{lineno:{len(str(n_lines))}d} ',
883 'linenumber')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400884 for element in self.theme_elements:
885 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400886 # event.widget.winfo_top_level().highlight_target.set(elem)
887 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400888 text.tag_bind(
889 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400890 text['state'] = 'disabled'
891 self.style.configure('frame_color_set.TFrame', borderwidth=1,
892 relief='solid')
893 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400894 frame_fg_bg_toggle = Frame(frame_custom)
895 self.button_set_color = Button(
896 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400897 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400898 self.targetlist = DynOptionMenu(
899 self.frame_color_set, self.highlight_target, None,
900 highlightthickness=0) #, command=self.set_highlight_targetBinding
901 self.fg_on = Radiobutton(
902 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
903 text='Foreground', command=self.set_color_sample_binding)
904 self.bg_on = Radiobutton(
905 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
906 text='Background', command=self.set_color_sample_binding)
907 self.fg_bg_toggle.set(1)
908 self.button_save_custom = Button(
909 frame_custom, text='Save as New Custom Theme',
910 command=self.save_as_new_theme)
911 # frame_theme.
912 theme_type_title = Label(frame_theme, text='Select : ')
913 self.builtin_theme_on = Radiobutton(
914 frame_theme, variable=self.theme_source, value=1,
915 command=self.set_theme_type, text='a Built-in Theme')
916 self.custom_theme_on = Radiobutton(
917 frame_theme, variable=self.theme_source, value=0,
918 command=self.set_theme_type, text='a Custom Theme')
919 self.builtinlist = DynOptionMenu(
920 frame_theme, self.builtin_name, None, command=None)
921 self.customlist = DynOptionMenu(
922 frame_theme, self.custom_name, None, command=None)
923 self.button_delete_custom = Button(
924 frame_theme, text='Delete Custom Theme',
925 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400926 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400927 # Pack widgets:
928 # body.
929 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500930 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400931 # frame_custom.
Tal Einat3221a632019-07-27 19:57:48 +0300932 self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400933 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
Tal Einat3221a632019-07-27 19:57:48 +0300934 sample_frame.pack(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400935 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
936 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
937 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
938 self.fg_on.pack(side=LEFT, anchor=E)
939 self.bg_on.pack(side=RIGHT, anchor=W)
940 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
941 # frame_theme.
942 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
943 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
944 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
945 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
946 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
947 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
948 self.theme_message.pack(side=TOP, fill=X, pady=5)
949
950 def load_theme_cfg(self):
951 """Load current configuration settings for the theme options.
952
953 Based on the theme_source toggle, the theme is set as
954 either builtin or custom and the initial widget values
955 reflect the current settings from idleConf.
956
957 Attributes updated:
958 theme_source: Set from idleConf.
959 builtinlist: List of default themes from idleConf.
960 customlist: List of custom themes from idleConf.
961 custom_theme_on: Disabled if there are no custom themes.
962 custom_theme: Message with additional information.
963 targetlist: Create menu from self.theme_elements.
964
965 Methods:
966 set_theme_type
967 paint_theme_sample
968 set_highlight_target
969 """
970 # Set current theme type radiobutton.
971 self.theme_source.set(idleConf.GetOption(
972 'main', 'Theme', 'default', type='bool', default=1))
973 # Set current theme.
974 current_option = idleConf.CurrentTheme()
975 # Load available theme option menus.
976 if self.theme_source.get(): # Default theme selected.
977 item_list = idleConf.GetSectionList('default', 'highlight')
978 item_list.sort()
979 self.builtinlist.SetMenu(item_list, current_option)
980 item_list = idleConf.GetSectionList('user', 'highlight')
981 item_list.sort()
982 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400983 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400984 self.custom_name.set('- no custom themes -')
985 else:
986 self.customlist.SetMenu(item_list, item_list[0])
987 else: # User theme selected.
988 item_list = idleConf.GetSectionList('user', 'highlight')
989 item_list.sort()
990 self.customlist.SetMenu(item_list, current_option)
991 item_list = idleConf.GetSectionList('default', 'highlight')
992 item_list.sort()
993 self.builtinlist.SetMenu(item_list, item_list[0])
994 self.set_theme_type()
995 # Load theme element option menu.
996 theme_names = list(self.theme_elements.keys())
997 theme_names.sort(key=lambda x: self.theme_elements[x][1])
998 self.targetlist.SetMenu(theme_names, theme_names[0])
999 self.paint_theme_sample()
1000 self.set_highlight_target()
1001
1002 def var_changed_builtin_name(self, *params):
1003 """Process new builtin theme selection.
1004
1005 Add the changed theme's name to the changed_items and recreate
1006 the sample with the values from the selected theme.
1007 """
1008 old_themes = ('IDLE Classic', 'IDLE New')
1009 value = self.builtin_name.get()
1010 if value not in old_themes:
1011 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
1012 changes.add_option('main', 'Theme', 'name', old_themes[0])
1013 changes.add_option('main', 'Theme', 'name2', value)
1014 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001015 else:
1016 changes.add_option('main', 'Theme', 'name', value)
1017 changes.add_option('main', 'Theme', 'name2', '')
1018 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001019 self.paint_theme_sample()
1020
1021 def var_changed_custom_name(self, *params):
1022 """Process new custom theme selection.
1023
1024 If a new custom theme is selected, add the name to the
1025 changed_items and apply the theme to the sample.
1026 """
1027 value = self.custom_name.get()
1028 if value != '- no custom themes -':
1029 changes.add_option('main', 'Theme', 'name', value)
1030 self.paint_theme_sample()
1031
1032 def var_changed_theme_source(self, *params):
1033 """Process toggle between builtin and custom theme.
1034
1035 Update the default toggle value and apply the newly
1036 selected theme type.
1037 """
1038 value = self.theme_source.get()
1039 changes.add_option('main', 'Theme', 'default', value)
1040 if value:
1041 self.var_changed_builtin_name()
1042 else:
1043 self.var_changed_custom_name()
1044
1045 def var_changed_color(self, *params):
1046 "Process change to color choice."
1047 self.on_new_color_set()
1048
1049 def var_changed_highlight_target(self, *params):
1050 "Process selection of new target tag for highlighting."
1051 self.set_highlight_target()
1052
1053 def set_theme_type(self):
1054 """Set available screen options based on builtin or custom theme.
1055
1056 Attributes accessed:
1057 theme_source
1058
1059 Attributes updated:
1060 builtinlist
1061 customlist
1062 button_delete_custom
1063 custom_theme_on
1064
1065 Called from:
1066 handler for builtin_theme_on and custom_theme_on
1067 delete_custom
1068 create_new
1069 load_theme_cfg
1070 """
1071 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001072 self.builtinlist['state'] = 'normal'
1073 self.customlist['state'] = 'disabled'
1074 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001075 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001076 self.builtinlist['state'] = 'disabled'
1077 self.custom_theme_on.state(('!disabled',))
1078 self.customlist['state'] = 'normal'
1079 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001080
1081 def get_color(self):
1082 """Handle button to select a new color for the target tag.
1083
1084 If a new color is selected while using a builtin theme, a
1085 name must be supplied to create a custom theme.
1086
1087 Attributes accessed:
1088 highlight_target
1089 frame_color_set
1090 theme_source
1091
1092 Attributes updated:
1093 color
1094
1095 Methods:
1096 get_new_theme_name
1097 create_new
1098 """
1099 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001100 prev_color = self.style.lookup(self.frame_color_set['style'],
1101 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001102 rgbTuplet, color_string = tkColorChooser.askcolor(
1103 parent=self, title='Pick new color for : '+target,
1104 initialcolor=prev_color)
1105 if color_string and (color_string != prev_color):
1106 # User didn't cancel and they chose a new color.
1107 if self.theme_source.get(): # Current theme is a built-in.
1108 message = ('Your changes will be saved as a new Custom Theme. '
1109 'Enter a name for your new Custom Theme below.')
1110 new_theme = self.get_new_theme_name(message)
1111 if not new_theme: # User cancelled custom theme creation.
1112 return
1113 else: # Create new custom theme based on previously active theme.
1114 self.create_new(new_theme)
1115 self.color.set(color_string)
1116 else: # Current theme is user defined.
1117 self.color.set(color_string)
1118
1119 def on_new_color_set(self):
1120 "Display sample of new color selection on the dialog."
1121 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001122 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001123 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1124 sample_element = self.theme_elements[self.highlight_target.get()][0]
1125 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1126 theme = self.custom_name.get()
1127 theme_element = sample_element + '-' + plane
1128 changes.add_option('highlight', theme, theme_element, new_color)
1129
1130 def get_new_theme_name(self, message):
1131 "Return name of new theme from query popup."
1132 used_names = (idleConf.GetSectionList('user', 'highlight') +
1133 idleConf.GetSectionList('default', 'highlight'))
1134 new_theme = SectionName(
1135 self, 'New Custom Theme', message, used_names).result
1136 return new_theme
1137
1138 def save_as_new_theme(self):
1139 """Prompt for new theme name and create the theme.
1140
1141 Methods:
1142 get_new_theme_name
1143 create_new
1144 """
1145 new_theme_name = self.get_new_theme_name('New Theme Name:')
1146 if new_theme_name:
1147 self.create_new(new_theme_name)
1148
1149 def create_new(self, new_theme_name):
1150 """Create a new custom theme with the given name.
1151
1152 Create the new theme based on the previously active theme
1153 with the current changes applied. Once it is saved, then
1154 activate the new theme.
1155
1156 Attributes accessed:
1157 builtin_name
1158 custom_name
1159
1160 Attributes updated:
1161 customlist
1162 theme_source
1163
1164 Method:
1165 save_new
1166 set_theme_type
1167 """
1168 if self.theme_source.get():
1169 theme_type = 'default'
1170 theme_name = self.builtin_name.get()
1171 else:
1172 theme_type = 'user'
1173 theme_name = self.custom_name.get()
1174 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1175 # Apply any of the old theme's unsaved changes to the new theme.
1176 if theme_name in changes['highlight']:
1177 theme_changes = changes['highlight'][theme_name]
1178 for element in theme_changes:
1179 new_theme[element] = theme_changes[element]
1180 # Save the new theme.
1181 self.save_new(new_theme_name, new_theme)
1182 # Change GUI over to the new theme.
1183 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1184 custom_theme_list.sort()
1185 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1186 self.theme_source.set(0)
1187 self.set_theme_type()
1188
1189 def set_highlight_target(self):
1190 """Set fg/bg toggle and color based on highlight tag target.
1191
1192 Instance variables accessed:
1193 highlight_target
1194
1195 Attributes updated:
1196 fg_on
1197 bg_on
1198 fg_bg_toggle
1199
1200 Methods:
1201 set_color_sample
1202
1203 Called from:
1204 var_changed_highlight_target
1205 load_theme_cfg
1206 """
1207 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001208 self.fg_on.state(('disabled',))
1209 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001210 self.fg_bg_toggle.set(1)
1211 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001212 self.fg_on.state(('!disabled',))
1213 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001214 self.fg_bg_toggle.set(1)
1215 self.set_color_sample()
1216
1217 def set_color_sample_binding(self, *args):
1218 """Change color sample based on foreground/background toggle.
1219
1220 Methods:
1221 set_color_sample
1222 """
1223 self.set_color_sample()
1224
1225 def set_color_sample(self):
1226 """Set the color of the frame background to reflect the selected target.
1227
1228 Instance variables accessed:
1229 theme_elements
1230 highlight_target
1231 fg_bg_toggle
1232 highlight_sample
1233
1234 Attributes updated:
1235 frame_color_set
1236 """
1237 # Set the color sample area.
1238 tag = self.theme_elements[self.highlight_target.get()][0]
1239 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1240 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001241 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001242
1243 def paint_theme_sample(self):
1244 """Apply the theme colors to each element tag in the sample text.
1245
1246 Instance attributes accessed:
1247 theme_elements
1248 theme_source
1249 builtin_name
1250 custom_name
1251
1252 Attributes updated:
1253 highlight_sample: Set the tag elements to the theme.
1254
1255 Methods:
1256 set_color_sample
1257
1258 Called from:
1259 var_changed_builtin_name
1260 var_changed_custom_name
1261 load_theme_cfg
1262 """
1263 if self.theme_source.get(): # Default theme
1264 theme = self.builtin_name.get()
1265 else: # User theme
1266 theme = self.custom_name.get()
1267 for element_title in self.theme_elements:
1268 element = self.theme_elements[element_title][0]
1269 colors = idleConf.GetHighlight(theme, element)
1270 if element == 'cursor': # Cursor sample needs special painting.
1271 colors['background'] = idleConf.GetHighlight(
Terry Jan Reedyc1419572019-03-22 18:23:41 -04001272 theme, 'normal')['background']
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001273 # Handle any unsaved changes to this theme.
1274 if theme in changes['highlight']:
1275 theme_dict = changes['highlight'][theme]
1276 if element + '-foreground' in theme_dict:
1277 colors['foreground'] = theme_dict[element + '-foreground']
1278 if element + '-background' in theme_dict:
1279 colors['background'] = theme_dict[element + '-background']
1280 self.highlight_sample.tag_config(element, **colors)
1281 self.set_color_sample()
1282
1283 def save_new(self, theme_name, theme):
1284 """Save a newly created theme to idleConf.
1285
1286 theme_name - string, the name of the new theme
1287 theme - dictionary containing the new theme
1288 """
Cheryl Sabelladd023ad2020-01-27 17:15:56 -05001289 idleConf.userCfg['highlight'].AddSection(theme_name)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001290 for element in theme:
1291 value = theme[element]
1292 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1293
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001294 def askyesno(self, *args, **kwargs):
1295 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001296 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001297
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001298 def delete_custom(self):
1299 """Handle event to delete custom theme.
1300
1301 The current theme is deactivated and the default theme is
1302 activated. The custom theme is permanently removed from
1303 the config file.
1304
1305 Attributes accessed:
1306 custom_name
1307
1308 Attributes updated:
1309 custom_theme_on
1310 customlist
1311 theme_source
1312 builtin_name
1313
1314 Methods:
1315 deactivate_current_config
1316 save_all_changed_extensions
1317 activate_config_changes
1318 set_theme_type
1319 """
1320 theme_name = self.custom_name.get()
1321 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001322 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001323 'Delete Theme', delmsg % theme_name, parent=self):
1324 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001325 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001326 # Remove theme from changes, config, and file.
1327 changes.delete_section('highlight', theme_name)
1328 # Reload user theme list.
1329 item_list = idleConf.GetSectionList('user', 'highlight')
1330 item_list.sort()
1331 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001332 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001333 self.customlist.SetMenu(item_list, '- no custom themes -')
1334 else:
1335 self.customlist.SetMenu(item_list, item_list[0])
1336 # Revert to default theme.
1337 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1338 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1339 # User can't back out of these changes, they must be applied now.
1340 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001341 self.cd.save_all_changed_extensions()
1342 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001343 self.set_theme_type()
1344
1345
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001346class KeysPage(Frame):
1347
1348 def __init__(self, master):
1349 super().__init__(master)
1350 self.cd = master.master
1351 self.create_page_keys()
1352 self.load_key_cfg()
1353
1354 def create_page_keys(self):
1355 """Return frame of widgets for Keys tab.
1356
1357 Enable users to provisionally change both individual and sets of
1358 keybindings (shortcut keys). Except for features implemented as
1359 extensions, keybindings are stored in complete sets called
1360 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1361 as far as the dialog is concerned. Any keyset can be used as the
1362 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1363
1364 Function load_key_cfg() initializes tk variables and keyset
1365 lists and calls load_keys_list for the current keyset.
1366 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1367 keyset_source, which controls if the current set of keybindings
1368 are from a builtin or custom keyset. DynOptionMenus builtinlist
1369 and customlist contain lists of the builtin and custom keysets,
1370 respectively, and the current item from each list is stored in
1371 vars builtin_name and custom_name.
1372
1373 Button delete_custom_keys invokes delete_custom_keys() to delete
1374 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1375 save_custom_keys invokes save_as_new_key_set() which calls
1376 get_new_keys_name() and create_new_key_set() to save a custom keyset
1377 and its keybindings to idleConf.userCfg['keys'].
1378
1379 Listbox bindingslist contains all of the keybindings for the
1380 selected keyset. The keybindings are loaded in load_keys_list()
1381 and are pairs of (event, [keys]) where keys can be a list
1382 of one or more key combinations to bind to the same event.
1383 Mouse button 1 click invokes on_bindingslist_select(), which
1384 allows button_new_keys to be clicked.
1385
1386 So, an item is selected in listbindings, which activates
1387 button_new_keys, and clicking button_new_keys calls function
1388 get_new_keys(). Function get_new_keys() gets the key mappings from the
1389 current keyset for the binding event item that was selected. The
1390 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001391 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001392 to be entered for that binding event. If the keys aren't
1393 changed, nothing happens. If the keys are changed and the keyset
1394 is a builtin, function get_new_keys_name() will be called
1395 for input of a custom keyset name. If no name is given, then the
1396 change to the keybinding will abort and no updates will be made. If
1397 a custom name is entered in the prompt or if the current keyset was
1398 already custom (and thus didn't require a prompt), then
1399 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1400 with the change to the event binding. The item listing in bindingslist
1401 is updated with the new keys. Var keybinding is also set which invokes
1402 the callback function, var_changed_keybinding, to add the change to
1403 the 'keys' or 'extensions' changes tracker based on the binding type.
1404
1405 Tk Variables:
1406 keybinding: Action/key bindings.
1407
1408 Methods:
1409 load_keys_list: Reload active set.
1410 create_new_key_set: Combine active keyset and changes.
1411 set_keys_type: Command for keyset_source.
1412 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1413 deactivate_current_config: Remove keys bindings in editors.
1414
1415 Widgets for KeysPage(frame): (*) widgets bound to self
1416 frame_key_sets: LabelFrame
1417 frames[0]: Frame
1418 (*)builtin_keyset_on: Radiobutton - var keyset_source
1419 (*)custom_keyset_on: Radiobutton - var keyset_source
1420 (*)builtinlist: DynOptionMenu - var builtin_name,
1421 func keybinding_selected
1422 (*)customlist: DynOptionMenu - var custom_name,
1423 func keybinding_selected
1424 (*)keys_message: Label
1425 frames[1]: Frame
1426 (*)button_delete_custom_keys: Button - delete_custom_keys
1427 (*)button_save_custom_keys: Button - save_as_new_key_set
1428 frame_custom: LabelFrame
1429 frame_target: Frame
1430 target_title: Label
1431 scroll_target_y: Scrollbar
1432 scroll_target_x: Scrollbar
1433 (*)bindingslist: ListBox - on_bindingslist_select
1434 (*)button_new_keys: Button - get_new_keys & ..._name
1435 """
1436 self.builtin_name = tracers.add(
1437 StringVar(self), self.var_changed_builtin_name)
1438 self.custom_name = tracers.add(
1439 StringVar(self), self.var_changed_custom_name)
1440 self.keyset_source = tracers.add(
1441 BooleanVar(self), self.var_changed_keyset_source)
1442 self.keybinding = tracers.add(
1443 StringVar(self), self.var_changed_keybinding)
1444
1445 # Create widgets:
1446 # body and section frames.
1447 frame_custom = LabelFrame(
1448 self, borderwidth=2, relief=GROOVE,
1449 text=' Custom Key Bindings ')
1450 frame_key_sets = LabelFrame(
1451 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1452 # frame_custom.
1453 frame_target = Frame(frame_custom)
1454 target_title = Label(frame_target, text='Action - Key(s)')
1455 scroll_target_y = Scrollbar(frame_target)
1456 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1457 self.bindingslist = Listbox(
1458 frame_target, takefocus=FALSE, exportselection=FALSE)
1459 self.bindingslist.bind('<ButtonRelease-1>',
1460 self.on_bindingslist_select)
1461 scroll_target_y['command'] = self.bindingslist.yview
1462 scroll_target_x['command'] = self.bindingslist.xview
1463 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1464 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1465 self.button_new_keys = Button(
1466 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001467 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001468 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001469 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001470 for i in range(2)]
1471 self.builtin_keyset_on = Radiobutton(
1472 frames[0], variable=self.keyset_source, value=1,
1473 command=self.set_keys_type, text='Use a Built-in Key Set')
1474 self.custom_keyset_on = Radiobutton(
1475 frames[0], variable=self.keyset_source, value=0,
1476 command=self.set_keys_type, text='Use a Custom Key Set')
1477 self.builtinlist = DynOptionMenu(
1478 frames[0], self.builtin_name, None, command=None)
1479 self.customlist = DynOptionMenu(
1480 frames[0], self.custom_name, None, command=None)
1481 self.button_delete_custom_keys = Button(
1482 frames[1], text='Delete Custom Key Set',
1483 command=self.delete_custom_keys)
1484 self.button_save_custom_keys = Button(
1485 frames[1], text='Save as New Custom Key Set',
1486 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001487 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001488
1489 # Pack widgets:
1490 # body.
1491 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1492 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1493 # frame_custom.
1494 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1495 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1496 # frame_target.
1497 frame_target.columnconfigure(0, weight=1)
1498 frame_target.rowconfigure(1, weight=1)
1499 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1500 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1501 scroll_target_y.grid(row=1, column=1, sticky=NS)
1502 scroll_target_x.grid(row=2, column=0, sticky=EW)
1503 # frame_key_sets.
1504 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1505 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1506 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1507 self.customlist.grid(row=1, column=1, sticky=NSEW)
1508 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1509 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1510 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1511 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1512 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1513
1514 def load_key_cfg(self):
1515 "Load current configuration settings for the keybinding options."
1516 # Set current keys type radiobutton.
1517 self.keyset_source.set(idleConf.GetOption(
1518 'main', 'Keys', 'default', type='bool', default=1))
1519 # Set current keys.
1520 current_option = idleConf.CurrentKeys()
1521 # Load available keyset option menus.
1522 if self.keyset_source.get(): # Default theme selected.
1523 item_list = idleConf.GetSectionList('default', 'keys')
1524 item_list.sort()
1525 self.builtinlist.SetMenu(item_list, current_option)
1526 item_list = idleConf.GetSectionList('user', 'keys')
1527 item_list.sort()
1528 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001529 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001530 self.custom_name.set('- no custom keys -')
1531 else:
1532 self.customlist.SetMenu(item_list, item_list[0])
1533 else: # User key set selected.
1534 item_list = idleConf.GetSectionList('user', 'keys')
1535 item_list.sort()
1536 self.customlist.SetMenu(item_list, current_option)
1537 item_list = idleConf.GetSectionList('default', 'keys')
1538 item_list.sort()
1539 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1540 self.set_keys_type()
1541 # Load keyset element list.
1542 keyset_name = idleConf.CurrentKeys()
1543 self.load_keys_list(keyset_name)
1544
1545 def var_changed_builtin_name(self, *params):
1546 "Process selection of builtin key set."
1547 old_keys = (
1548 'IDLE Classic Windows',
1549 'IDLE Classic Unix',
1550 'IDLE Classic Mac',
1551 'IDLE Classic OSX',
1552 )
1553 value = self.builtin_name.get()
1554 if value not in old_keys:
1555 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1556 changes.add_option('main', 'Keys', 'name', old_keys[0])
1557 changes.add_option('main', 'Keys', 'name2', value)
1558 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001559 else:
1560 changes.add_option('main', 'Keys', 'name', value)
1561 changes.add_option('main', 'Keys', 'name2', '')
1562 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001563 self.load_keys_list(value)
1564
1565 def var_changed_custom_name(self, *params):
1566 "Process selection of custom key set."
1567 value = self.custom_name.get()
1568 if value != '- no custom keys -':
1569 changes.add_option('main', 'Keys', 'name', value)
1570 self.load_keys_list(value)
1571
1572 def var_changed_keyset_source(self, *params):
1573 "Process toggle between builtin key set and custom key set."
1574 value = self.keyset_source.get()
1575 changes.add_option('main', 'Keys', 'default', value)
1576 if value:
1577 self.var_changed_builtin_name()
1578 else:
1579 self.var_changed_custom_name()
1580
1581 def var_changed_keybinding(self, *params):
1582 "Store change to a keybinding."
1583 value = self.keybinding.get()
1584 key_set = self.custom_name.get()
1585 event = self.bindingslist.get(ANCHOR).split()[0]
1586 if idleConf.IsCoreBinding(event):
1587 changes.add_option('keys', key_set, event, value)
1588 else: # Event is an extension binding.
1589 ext_name = idleConf.GetExtnNameForEvent(event)
1590 ext_keybind_section = ext_name + '_cfgBindings'
1591 changes.add_option('extensions', ext_keybind_section, event, value)
1592
1593 def set_keys_type(self):
1594 "Set available screen options based on builtin or custom key set."
1595 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001596 self.builtinlist['state'] = 'normal'
1597 self.customlist['state'] = 'disabled'
1598 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001599 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001600 self.builtinlist['state'] = 'disabled'
1601 self.custom_keyset_on.state(('!disabled',))
1602 self.customlist['state'] = 'normal'
1603 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001604
1605 def get_new_keys(self):
1606 """Handle event to change key binding for selected line.
1607
1608 A selection of a key/binding in the list of current
1609 bindings pops up a dialog to enter a new binding. If
1610 the current key set is builtin and a binding has
1611 changed, then a name for a custom key set needs to be
1612 entered for the change to be applied.
1613 """
1614 list_index = self.bindingslist.index(ANCHOR)
1615 binding = self.bindingslist.get(list_index)
1616 bind_name = binding.split()[0]
1617 if self.keyset_source.get():
1618 current_key_set_name = self.builtin_name.get()
1619 else:
1620 current_key_set_name = self.custom_name.get()
1621 current_bindings = idleConf.GetCurrentKeySet()
1622 if current_key_set_name in changes['keys']: # unsaved changes
1623 key_set_changes = changes['keys'][current_key_set_name]
1624 for event in key_set_changes:
1625 current_bindings[event] = key_set_changes[event].split()
1626 current_key_sequences = list(current_bindings.values())
1627 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1628 current_key_sequences).result
1629 if new_keys:
1630 if self.keyset_source.get(): # Current key set is a built-in.
1631 message = ('Your changes will be saved as a new Custom Key Set.'
1632 ' Enter a name for your new Custom Key Set below.')
1633 new_keyset = self.get_new_keys_name(message)
1634 if not new_keyset: # User cancelled custom key set creation.
1635 self.bindingslist.select_set(list_index)
1636 self.bindingslist.select_anchor(list_index)
1637 return
1638 else: # Create new custom key set based on previously active key set.
1639 self.create_new_key_set(new_keyset)
1640 self.bindingslist.delete(list_index)
1641 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1642 self.bindingslist.select_set(list_index)
1643 self.bindingslist.select_anchor(list_index)
1644 self.keybinding.set(new_keys)
1645 else:
1646 self.bindingslist.select_set(list_index)
1647 self.bindingslist.select_anchor(list_index)
1648
1649 def get_new_keys_name(self, message):
1650 "Return new key set name from query popup."
1651 used_names = (idleConf.GetSectionList('user', 'keys') +
1652 idleConf.GetSectionList('default', 'keys'))
1653 new_keyset = SectionName(
1654 self, 'New Custom Key Set', message, used_names).result
1655 return new_keyset
1656
1657 def save_as_new_key_set(self):
1658 "Prompt for name of new key set and save changes using that name."
1659 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1660 if new_keys_name:
1661 self.create_new_key_set(new_keys_name)
1662
1663 def on_bindingslist_select(self, event):
1664 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001665 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001666
1667 def create_new_key_set(self, new_key_set_name):
1668 """Create a new custom key set with the given name.
1669
1670 Copy the bindings/keys from the previously active keyset
1671 to the new keyset and activate the new custom keyset.
1672 """
1673 if self.keyset_source.get():
1674 prev_key_set_name = self.builtin_name.get()
1675 else:
1676 prev_key_set_name = self.custom_name.get()
1677 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1678 new_keys = {}
1679 for event in prev_keys: # Add key set to changed items.
1680 event_name = event[2:-2] # Trim off the angle brackets.
1681 binding = ' '.join(prev_keys[event])
1682 new_keys[event_name] = binding
1683 # Handle any unsaved changes to prev key set.
1684 if prev_key_set_name in changes['keys']:
1685 key_set_changes = changes['keys'][prev_key_set_name]
1686 for event in key_set_changes:
1687 new_keys[event] = key_set_changes[event]
1688 # Save the new key set.
1689 self.save_new_key_set(new_key_set_name, new_keys)
1690 # Change GUI over to the new key set.
1691 custom_key_list = idleConf.GetSectionList('user', 'keys')
1692 custom_key_list.sort()
1693 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1694 self.keyset_source.set(0)
1695 self.set_keys_type()
1696
1697 def load_keys_list(self, keyset_name):
1698 """Reload the list of action/key binding pairs for the active key set.
1699
1700 An action/key binding can be selected to change the key binding.
1701 """
1702 reselect = False
1703 if self.bindingslist.curselection():
1704 reselect = True
1705 list_index = self.bindingslist.index(ANCHOR)
1706 keyset = idleConf.GetKeySet(keyset_name)
1707 bind_names = list(keyset.keys())
1708 bind_names.sort()
1709 self.bindingslist.delete(0, END)
1710 for bind_name in bind_names:
1711 key = ' '.join(keyset[bind_name])
1712 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1713 if keyset_name in changes['keys']:
1714 # Handle any unsaved changes to this key set.
1715 if bind_name in changes['keys'][keyset_name]:
1716 key = changes['keys'][keyset_name][bind_name]
1717 self.bindingslist.insert(END, bind_name+' - '+key)
1718 if reselect:
1719 self.bindingslist.see(list_index)
1720 self.bindingslist.select_set(list_index)
1721 self.bindingslist.select_anchor(list_index)
1722
1723 @staticmethod
1724 def save_new_key_set(keyset_name, keyset):
1725 """Save a newly created core key set.
1726
1727 Add keyset to idleConf.userCfg['keys'], not to disk.
1728 If the keyset doesn't exist, it is created. The
1729 binding/keys are taken from the keyset argument.
1730
1731 keyset_name - string, the name of the new key set
1732 keyset - dictionary containing the new keybindings
1733 """
Cheryl Sabelladd023ad2020-01-27 17:15:56 -05001734 idleConf.userCfg['keys'].AddSection(keyset_name)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001735 for event in keyset:
1736 value = keyset[event]
1737 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1738
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001739 def askyesno(self, *args, **kwargs):
1740 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001741 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001742
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001743 def delete_custom_keys(self):
1744 """Handle event to delete a custom key set.
1745
1746 Applying the delete deactivates the current configuration and
1747 reverts to the default. The custom key set is permanently
1748 deleted from the config file.
1749 """
1750 keyset_name = self.custom_name.get()
1751 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001752 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001753 'Delete Key Set', delmsg % keyset_name, parent=self):
1754 return
1755 self.cd.deactivate_current_config()
1756 # Remove key set from changes, config, and file.
1757 changes.delete_section('keys', keyset_name)
1758 # Reload user key set list.
1759 item_list = idleConf.GetSectionList('user', 'keys')
1760 item_list.sort()
1761 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001762 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001763 self.customlist.SetMenu(item_list, '- no custom keys -')
1764 else:
1765 self.customlist.SetMenu(item_list, item_list[0])
1766 # Revert to default key set.
1767 self.keyset_source.set(idleConf.defaultCfg['main']
Tal Einat604e7b92018-09-25 15:10:14 +03001768 .Get('Keys', 'default'))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001769 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
Tal Einat604e7b92018-09-25 15:10:14 +03001770 or idleConf.default_keys())
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001771 # User can't back out of these changes, they must be applied now.
1772 changes.save_all()
1773 self.cd.save_all_changed_extensions()
1774 self.cd.activate_config_changes()
1775 self.set_keys_type()
1776
1777
csabellae8eb17b2017-07-30 18:39:17 -04001778class GenPage(Frame):
1779
csabella6f446be2017-08-01 00:24:07 -04001780 def __init__(self, master):
1781 super().__init__(master)
Tal Einat1ebee372019-07-23 13:02:11 +03001782
1783 self.init_validators()
csabellae8eb17b2017-07-30 18:39:17 -04001784 self.create_page_general()
1785 self.load_general_cfg()
1786
Tal Einat1ebee372019-07-23 13:02:11 +03001787 def init_validators(self):
1788 digits_or_empty_re = re.compile(r'[0-9]*')
1789 def is_digits_or_empty(s):
1790 "Return 's is blank or contains only digits'"
1791 return digits_or_empty_re.fullmatch(s) is not None
1792 self.digits_only = (self.register(is_digits_or_empty), '%P',)
1793
csabellae8eb17b2017-07-30 18:39:17 -04001794 def create_page_general(self):
1795 """Return frame of widgets for General tab.
1796
1797 Enable users to provisionally change general options. Function
Terry Jan Reedy0acb6462019-07-30 18:14:58 -04001798 load_general_cfg initializes tk variables and helplist using
csabellae8eb17b2017-07-30 18:39:17 -04001799 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1800 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1801 set var autosave. Entry boxes win_width_int and win_height_int
1802 set var win_width and win_height. Setting var_name invokes the
1803 default callback that adds option to changes.
1804
1805 Helplist: load_general_cfg loads list user_helplist with
1806 name, position pairs and copies names to listbox helplist.
1807 Clicking a name invokes help_source selected. Clicking
1808 button_helplist_name invokes helplist_item_name, which also
1809 changes user_helplist. These functions all call
1810 set_add_delete_state. All but load call update_help_changes to
1811 rewrite changes['main']['HelpFiles'].
1812
Cheryl Sabella2f896462017-08-14 21:21:43 -04001813 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001814 frame_window: LabelFrame
1815 frame_run: Frame
1816 startup_title: Label
1817 (*)startup_editor_on: Radiobutton - startup_edit
1818 (*)startup_shell_on: Radiobutton - startup_edit
1819 frame_win_size: Frame
1820 win_size_title: Label
1821 win_width_title: Label
1822 (*)win_width_int: Entry - win_width
1823 win_height_title: Label
1824 (*)win_height_int: Entry - win_height
Zackery Spytz9c284492019-11-13 00:13:33 -07001825 frame_cursor_blink: Frame
1826 cursor_blink_title: Label
1827 (*)cursor_blink_bool: Checkbutton - cursor_blink
Cheryl Sabella845d8642018-02-04 18:15:21 -05001828 frame_autocomplete: Frame
1829 auto_wait_title: Label
1830 (*)auto_wait_int: Entry - autocomplete_wait
1831 frame_paren1: Frame
1832 paren_style_title: Label
1833 (*)paren_style_type: OptionMenu - paren_style
1834 frame_paren2: Frame
1835 paren_time_title: Label
1836 (*)paren_flash_time: Entry - flash_delay
1837 (*)bell_on: Checkbutton - paren_bell
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001838 frame_editor: LabelFrame
1839 frame_save: Frame
1840 run_save_title: Label
1841 (*)save_ask_on: Radiobutton - autosave
1842 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella845d8642018-02-04 18:15:21 -05001843 frame_format: Frame
1844 format_width_title: Label
1845 (*)format_width_int: Entry - format_width
Tal Einat7123ea02019-07-23 15:22:11 +03001846 frame_line_numbers_default: Frame
1847 line_numbers_default_title: Label
1848 (*)line_numbers_default_bool: Checkbutton - line_numbers_default
Cheryl Sabella845d8642018-02-04 18:15:21 -05001849 frame_context: Frame
1850 context_title: Label
1851 (*)context_int: Entry - context_lines
Tal Einat604e7b92018-09-25 15:10:14 +03001852 frame_shell: LabelFrame
1853 frame_auto_squeeze_min_lines: Frame
1854 auto_squeeze_min_lines_title: Label
1855 (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines
Cheryl Sabella2f896462017-08-14 21:21:43 -04001856 frame_help: LabelFrame
1857 frame_helplist: Frame
1858 frame_helplist_buttons: Frame
1859 (*)button_helplist_edit
1860 (*)button_helplist_add
1861 (*)button_helplist_remove
1862 (*)helplist: ListBox
1863 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001864 """
wohlganger58fc71c2017-09-10 16:19:47 -05001865 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001866 self.startup_edit = tracers.add(
1867 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001868 self.win_width = tracers.add(
1869 StringVar(self), ('main', 'EditorWindow', 'width'))
1870 self.win_height = tracers.add(
1871 StringVar(self), ('main', 'EditorWindow', 'height'))
Zackery Spytz9c284492019-11-13 00:13:33 -07001872 self.cursor_blink = tracers.add(
1873 BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink'))
wohlganger58fc71c2017-09-10 16:19:47 -05001874 self.autocomplete_wait = tracers.add(
1875 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1876 self.paren_style = tracers.add(
1877 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1878 self.flash_delay = tracers.add(
1879 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1880 self.paren_bell = tracers.add(
1881 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001882
Tal Einat604e7b92018-09-25 15:10:14 +03001883 self.auto_squeeze_min_lines = tracers.add(
1884 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
1885
wohlganger58fc71c2017-09-10 16:19:47 -05001886 self.autosave = tracers.add(
1887 IntVar(self), ('main', 'General', 'autosave'))
1888 self.format_width = tracers.add(
1889 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
Tal Einat7123ea02019-07-23 15:22:11 +03001890 self.line_numbers_default = tracers.add(
1891 BooleanVar(self),
1892 ('main', 'EditorWindow', 'line-numbers-default'))
wohlganger58fc71c2017-09-10 16:19:47 -05001893 self.context_lines = tracers.add(
Cheryl Sabella29996a12018-06-01 19:23:00 -04001894 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
wohlganger58fc71c2017-09-10 16:19:47 -05001895
1896 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001897 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001898 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1899 text=' Window Preferences')
1900 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1901 text=' Editor Preferences')
Tal Einat604e7b92018-09-25 15:10:14 +03001902 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
1903 text=' Shell Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001904 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
Tal Einat604e7b92018-09-25 15:10:14 +03001905 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001906 # Frame_window.
1907 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001908 startup_title = Label(frame_run, text='At Startup')
1909 self.startup_editor_on = Radiobutton(
1910 frame_run, variable=self.startup_edit, value=1,
1911 text="Open Edit Window")
1912 self.startup_shell_on = Radiobutton(
1913 frame_run, variable=self.startup_edit, value=0,
1914 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001915
wohlganger58fc71c2017-09-10 16:19:47 -05001916 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001917 win_size_title = Label(
1918 frame_win_size, text='Initial Window Size (in characters)')
1919 win_width_title = Label(frame_win_size, text='Width')
1920 self.win_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001921 frame_win_size, textvariable=self.win_width, width=3,
1922 validatecommand=self.digits_only, validate='key',
1923 )
csabellae8eb17b2017-07-30 18:39:17 -04001924 win_height_title = Label(frame_win_size, text='Height')
1925 self.win_height_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001926 frame_win_size, textvariable=self.win_height, width=3,
1927 validatecommand=self.digits_only, validate='key',
1928 )
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001929
Zackery Spytz9c284492019-11-13 00:13:33 -07001930 frame_cursor_blink = Frame(frame_window, borderwidth=0)
1931 cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink')
1932 self.cursor_blink_bool = Checkbutton(frame_cursor_blink,
1933 variable=self.cursor_blink, width=1)
1934
wohlganger58fc71c2017-09-10 16:19:47 -05001935 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1936 auto_wait_title = Label(frame_autocomplete,
1937 text='Completions Popup Wait (milliseconds)')
1938 self.auto_wait_int = Entry(frame_autocomplete, width=6,
Tal Einat1ebee372019-07-23 13:02:11 +03001939 textvariable=self.autocomplete_wait,
1940 validatecommand=self.digits_only,
1941 validate='key',
1942 )
wohlganger58fc71c2017-09-10 16:19:47 -05001943
1944 frame_paren1 = Frame(frame_window, borderwidth=0)
1945 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1946 self.paren_style_type = OptionMenu(
1947 frame_paren1, self.paren_style, 'expression',
1948 "opener","parens","expression")
1949 frame_paren2 = Frame(frame_window, borderwidth=0)
1950 paren_time_title = Label(
1951 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1952 '(0 is until next input)')
1953 self.paren_flash_time = Entry(
1954 frame_paren2, textvariable=self.flash_delay, width=6)
1955 self.bell_on = Checkbutton(
1956 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1957
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001958 # Frame_editor.
1959 frame_save = Frame(frame_editor, borderwidth=0)
1960 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1961 self.save_ask_on = Radiobutton(
1962 frame_save, variable=self.autosave, value=0,
1963 text="Prompt to Save")
1964 self.save_auto_on = Radiobutton(
1965 frame_save, variable=self.autosave, value=1,
1966 text='No Prompt')
1967
wohlganger58fc71c2017-09-10 16:19:47 -05001968 frame_format = Frame(frame_editor, borderwidth=0)
1969 format_width_title = Label(frame_format,
1970 text='Format Paragraph Max Width')
1971 self.format_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001972 frame_format, textvariable=self.format_width, width=4,
1973 validatecommand=self.digits_only, validate='key',
1974 )
wohlganger58fc71c2017-09-10 16:19:47 -05001975
Tal Einat7123ea02019-07-23 15:22:11 +03001976 frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
1977 line_numbers_default_title = Label(
1978 frame_line_numbers_default, text='Show line numbers in new windows')
1979 self.line_numbers_default_bool = Checkbutton(
1980 frame_line_numbers_default,
1981 variable=self.line_numbers_default,
1982 width=1)
1983
wohlganger58fc71c2017-09-10 16:19:47 -05001984 frame_context = Frame(frame_editor, borderwidth=0)
Cheryl Sabella29996a12018-06-01 19:23:00 -04001985 context_title = Label(frame_context, text='Max Context Lines :')
wohlganger58fc71c2017-09-10 16:19:47 -05001986 self.context_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001987 frame_context, textvariable=self.context_lines, width=3,
1988 validatecommand=self.digits_only, validate='key',
1989 )
wohlganger58fc71c2017-09-10 16:19:47 -05001990
Tal Einat604e7b92018-09-25 15:10:14 +03001991 # Frame_shell.
1992 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
1993 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
1994 text='Auto-Squeeze Min. Lines:')
1995 self.auto_squeeze_min_lines_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001996 frame_auto_squeeze_min_lines, width=4,
1997 textvariable=self.auto_squeeze_min_lines,
1998 validatecommand=self.digits_only, validate='key',
1999 )
wohlganger58fc71c2017-09-10 16:19:47 -05002000
csabellae8eb17b2017-07-30 18:39:17 -04002001 # frame_help.
2002 frame_helplist = Frame(frame_help)
2003 frame_helplist_buttons = Frame(frame_helplist)
2004 self.helplist = Listbox(
2005 frame_helplist, height=5, takefocus=True,
2006 exportselection=FALSE)
2007 scroll_helplist = Scrollbar(frame_helplist)
2008 scroll_helplist['command'] = self.helplist.yview
2009 self.helplist['yscrollcommand'] = scroll_helplist.set
2010 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
2011 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04002012 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04002013 width=8, command=self.helplist_item_edit)
2014 self.button_helplist_add = Button(
2015 frame_helplist_buttons, text='Add',
2016 width=8, command=self.helplist_item_add)
2017 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04002018 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04002019 width=8, command=self.helplist_item_remove)
2020
2021 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002022 # Body.
2023 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2024 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Tal Einat604e7b92018-09-25 15:10:14 +03002025 frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04002026 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2027 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002028 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002029 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2030 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2031 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04002032 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002033 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002034 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2035 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2036 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
2037 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2038 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
Zackery Spytz9c284492019-11-13 00:13:33 -07002039 # frame_cursor_blink.
2040 frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X)
2041 cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2042 self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002043 # frame_autocomplete.
2044 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
2045 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2046 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
2047 # frame_paren.
2048 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
2049 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2050 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
2051 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
2052 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
2053 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
2054 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
2055
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002056 # frame_save.
2057 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
2058 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2059 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2060 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002061 # frame_format.
2062 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
2063 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2064 self.format_width_int.pack(side=TOP, padx=10, pady=5)
Tal Einat7123ea02019-07-23 15:22:11 +03002065 # frame_line_numbers_default.
2066 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
2067 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2068 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002069 # frame_context.
2070 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
2071 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2072 self.context_int.pack(side=TOP, padx=5, pady=5)
2073
Tal Einat604e7b92018-09-25 15:10:14 +03002074 # frame_auto_squeeze_min_lines
2075 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
2076 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2077 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
2078
csabellae8eb17b2017-07-30 18:39:17 -04002079 # frame_help.
2080 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
2081 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2082 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
2083 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
2084 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
2085 self.button_helplist_add.pack(side=TOP, anchor=W)
2086 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
2087
2088 def load_general_cfg(self):
2089 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05002090 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04002091 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05002092 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04002093 self.win_width.set(idleConf.GetOption(
2094 'main', 'EditorWindow', 'width', type='int'))
2095 self.win_height.set(idleConf.GetOption(
2096 'main', 'EditorWindow', 'height', type='int'))
Zackery Spytz9c284492019-11-13 00:13:33 -07002097 self.cursor_blink.set(idleConf.GetOption(
2098 'main', 'EditorWindow', 'cursor-blink', type='bool'))
wohlganger58fc71c2017-09-10 16:19:47 -05002099 self.autocomplete_wait.set(idleConf.GetOption(
2100 'extensions', 'AutoComplete', 'popupwait', type='int'))
2101 self.paren_style.set(idleConf.GetOption(
2102 'extensions', 'ParenMatch', 'style'))
2103 self.flash_delay.set(idleConf.GetOption(
2104 'extensions', 'ParenMatch', 'flash-delay', type='int'))
2105 self.paren_bell.set(idleConf.GetOption(
2106 'extensions', 'ParenMatch', 'bell'))
2107
2108 # Set variables for editor windows.
2109 self.autosave.set(idleConf.GetOption(
2110 'main', 'General', 'autosave', default=0, type='bool'))
2111 self.format_width.set(idleConf.GetOption(
2112 'extensions', 'FormatParagraph', 'max-width', type='int'))
Tal Einat7123ea02019-07-23 15:22:11 +03002113 self.line_numbers_default.set(idleConf.GetOption(
2114 'main', 'EditorWindow', 'line-numbers-default', type='bool'))
wohlganger58fc71c2017-09-10 16:19:47 -05002115 self.context_lines.set(idleConf.GetOption(
Cheryl Sabella29996a12018-06-01 19:23:00 -04002116 'extensions', 'CodeContext', 'maxlines', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002117
Tal Einat604e7b92018-09-25 15:10:14 +03002118 # Set variables for shell windows.
2119 self.auto_squeeze_min_lines.set(idleConf.GetOption(
2120 'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
2121
csabellae8eb17b2017-07-30 18:39:17 -04002122 # Set additional help sources.
2123 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2124 self.helplist.delete(0, 'end')
2125 for help_item in self.user_helplist:
2126 self.helplist.insert(END, help_item[0])
2127 self.set_add_delete_state()
2128
2129 def help_source_selected(self, event):
2130 "Handle event for selecting additional help."
2131 self.set_add_delete_state()
2132
2133 def set_add_delete_state(self):
2134 "Toggle the state for the help list buttons based on list entries."
2135 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002136 self.button_helplist_edit.state(('disabled',))
2137 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002138 else: # Some entries.
2139 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002140 self.button_helplist_edit.state(('!disabled',))
2141 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002142 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002143 self.button_helplist_edit.state(('disabled',))
2144 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002145
2146 def helplist_item_add(self):
2147 """Handle add button for the help list.
2148
2149 Query for name and location of new help sources and add
2150 them to the list.
2151 """
2152 help_source = HelpSource(self, 'New Help Source').result
2153 if help_source:
2154 self.user_helplist.append(help_source)
2155 self.helplist.insert(END, help_source[0])
2156 self.update_help_changes()
2157
2158 def helplist_item_edit(self):
2159 """Handle edit button for the help list.
2160
2161 Query with existing help source information and update
2162 config if the values are changed.
2163 """
2164 item_index = self.helplist.index(ANCHOR)
2165 help_source = self.user_helplist[item_index]
2166 new_help_source = HelpSource(
2167 self, 'Edit Help Source',
2168 menuitem=help_source[0],
2169 filepath=help_source[1],
2170 ).result
2171 if new_help_source and new_help_source != help_source:
2172 self.user_helplist[item_index] = new_help_source
2173 self.helplist.delete(item_index)
2174 self.helplist.insert(item_index, new_help_source[0])
2175 self.update_help_changes()
2176 self.set_add_delete_state() # Selected will be un-selected
2177
2178 def helplist_item_remove(self):
2179 """Handle remove button for the help list.
2180
2181 Delete the help list item from config.
2182 """
2183 item_index = self.helplist.index(ANCHOR)
2184 del(self.user_helplist[item_index])
2185 self.helplist.delete(item_index)
2186 self.update_help_changes()
2187 self.set_add_delete_state()
2188
2189 def update_help_changes(self):
2190 "Clear and rebuild the HelpFiles section in changes"
2191 changes['main']['HelpFiles'] = {}
2192 for num in range(1, len(self.user_helplist) + 1):
2193 changes.add_option(
2194 'main', 'HelpFiles', str(num),
2195 ';'.join(self.user_helplist[num-1][:2]))
2196
2197
csabella45bf7232017-07-26 19:09:58 -04002198class VarTrace:
2199 """Maintain Tk variables trace state."""
2200
2201 def __init__(self):
2202 """Store Tk variables and callbacks.
2203
2204 untraced: List of tuples (var, callback)
2205 that do not have the callback attached
2206 to the Tk var.
2207 traced: List of tuples (var, callback) where
2208 that callback has been attached to the var.
2209 """
2210 self.untraced = []
2211 self.traced = []
2212
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002213 def clear(self):
2214 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002215 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002216 self.untraced.clear()
2217 self.traced.clear()
2218
csabella45bf7232017-07-26 19:09:58 -04002219 def add(self, var, callback):
2220 """Add (var, callback) tuple to untraced list.
2221
2222 Args:
2223 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002224 callback: Either function name to be used as a callback
2225 or a tuple with IdleConf config-type, section, and
2226 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002227
2228 Return:
2229 Tk variable instance.
2230 """
2231 if isinstance(callback, tuple):
2232 callback = self.make_callback(var, callback)
2233 self.untraced.append((var, callback))
2234 return var
2235
2236 @staticmethod
2237 def make_callback(var, config):
2238 "Return default callback function to add values to changes instance."
2239 def default_callback(*params):
2240 "Add config values to changes instance."
2241 changes.add_option(*config, var.get())
2242 return default_callback
2243
2244 def attach(self):
2245 "Attach callback to all vars that are not traced."
2246 while self.untraced:
2247 var, callback = self.untraced.pop()
2248 var.trace_add('write', callback)
2249 self.traced.append((var, callback))
2250
2251 def detach(self):
2252 "Remove callback from traced vars."
2253 while self.traced:
2254 var, callback = self.traced.pop()
2255 var.trace_remove('write', var.trace_info()[0][1])
2256 self.untraced.append((var, callback))
2257
2258
csabella5b591542017-07-28 14:40:59 -04002259tracers = VarTrace()
2260
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002261help_common = '''\
2262When you click either the Apply or Ok buttons, settings in this
2263dialog that are different from IDLE's default are saved in
2264a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002265these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002266machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002267'''
2268help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002269 'Fonts/Tabs':'''
2270Font sample: This shows what a selection of Basic Multilingual Plane
2271unicode characters look like for the current font selection. If the
2272selected font does not define a character, Tk attempts to find another
2273font that does. Substitute glyphs depend on what is available on a
2274particular system and will not necessarily have the same size as the
2275font selected. Line contains 20 characters up to Devanagari, 14 for
2276Tamil, and 10 for East Asia.
2277
2278Hebrew and Arabic letters should display right to left, starting with
2279alef, \u05d0 and \u0627. Arabic digits display left to right. The
2280Devanagari and Tamil lines start with digits. The East Asian lines
2281are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2282Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002283
2284You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002285''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002286 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002287Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002288The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002289be used with older IDLE releases if it is saved as a custom
2290theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002291''',
2292 'Keys': '''
2293Keys:
2294The IDLE Modern Unix key set is new in June 2016. It can only
2295be used with older IDLE releases if it is saved as a custom
2296key set, with a different name.
2297''',
wohlganger58fc71c2017-09-10 16:19:47 -05002298 'General': '''
2299General:
wohlgangerfae2c352017-06-27 21:36:23 -05002300
penguindustin96466302019-05-06 14:57:17 -04002301AutoComplete: Popupwait is milliseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002302cursor movement, before popping up completion box. Key char is '.' after
2303identifier or a '/' (or '\\' on Windows) within a string.
2304
2305FormatParagraph: Max-width is max chars in lines after re-formatting.
2306Use with paragraphs in both strings and comment blocks.
2307
2308ParenMatch: Style indicates what is highlighted when closer is entered:
2309'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2310'expression' (default) - also everything in between. Flash-delay is how
2311long to highlight if cursor is not moved (0 means forever).
Cheryl Sabella29996a12018-06-01 19:23:00 -04002312
2313CodeContext: Maxlines is the maximum number of code context lines to
2314display when Code Context is turned on for an editor window.
Tal Einat604e7b92018-09-25 15:10:14 +03002315
2316Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
2317of output to automatically "squeeze".
wohlgangerfae2c352017-06-27 21:36:23 -05002318'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002319}
2320
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002321
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002322def is_int(s):
2323 "Return 's is blank or represents an int'"
2324 if not s:
2325 return True
2326 try:
2327 int(s)
2328 return True
2329 except ValueError:
2330 return False
2331
2332
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002333class VerticalScrolledFrame(Frame):
2334 """A pure Tkinter vertically scrollable frame.
2335
2336 * Use the 'interior' attribute to place widgets inside the scrollable frame
2337 * Construct and pack/place/grid normally
2338 * This frame only allows vertical scrolling
2339 """
2340 def __init__(self, parent, *args, **kw):
2341 Frame.__init__(self, parent, *args, **kw)
2342
csabella7eb58832017-07-04 21:30:58 -04002343 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002344 vscrollbar = Scrollbar(self, orient=VERTICAL)
2345 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002346 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002347 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002348 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2349 vscrollbar.config(command=canvas.yview)
2350
csabella7eb58832017-07-04 21:30:58 -04002351 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002352 canvas.xview_moveto(0)
2353 canvas.yview_moveto(0)
2354
csabella7eb58832017-07-04 21:30:58 -04002355 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002356 self.interior = interior = Frame(canvas)
2357 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2358
csabella7eb58832017-07-04 21:30:58 -04002359 # Track changes to the canvas and frame width and sync them,
2360 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002361 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002362 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002363 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2364 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002365 interior.bind('<Configure>', _configure_interior)
2366
2367 def _configure_canvas(event):
2368 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002369 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002370 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2371 canvas.bind('<Configure>', _configure_canvas)
2372
2373 return
2374
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002375
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002376if __name__ == '__main__':
Terry Jan Reedy4d921582018-06-19 19:12:52 -04002377 from unittest import main
2378 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
2379
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002380 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002381 run(ConfigDialog)