blob: 9d5c2cde04b2404e5c72f61d33cd1321543b7402 [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
Terry Jan Reedy96ce2272020-02-10 20:08:58 -0500609 # Set sorted no-duplicate editor font selection list and font_name.
610 fonts = sorted(set(tkFont.families(self)))
csabella9397e2a2017-07-30 13:34:25 -0400611 for font in fonts:
612 self.fontlist.insert(END, font)
613 self.font_name.set(font_name)
614 lc_fonts = [s.lower() for s in fonts]
615 try:
616 current_font_index = lc_fonts.index(font_name)
617 self.fontlist.see(current_font_index)
618 self.fontlist.select_set(current_font_index)
619 self.fontlist.select_anchor(current_font_index)
620 self.fontlist.activate(current_font_index)
621 except ValueError:
622 pass
623 # Set font size dropdown.
624 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
625 '16', '18', '20', '22', '25', '29', '34', '40'),
626 font_size)
627 # Set font weight.
628 self.font_bold.set(font_bold)
629 self.set_samples()
630
631 def var_changed_font(self, *params):
632 """Store changes to font attributes.
633
634 When one font attribute changes, save them all, as they are
635 not independent from each other. In particular, when we are
636 overriding the default font, we need to write out everything.
637 """
638 value = self.font_name.get()
639 changes.add_option('main', 'EditorWindow', 'font', value)
640 value = self.font_size.get()
641 changes.add_option('main', 'EditorWindow', 'font-size', value)
642 value = self.font_bold.get()
643 changes.add_option('main', 'EditorWindow', 'font-bold', value)
644 self.set_samples()
645
646 def on_fontlist_select(self, event):
647 """Handle selecting a font from the list.
648
649 Event can result from either mouse click or Up or Down key.
650 Set font_name and example displays to selection.
651 """
652 font = self.fontlist.get(
653 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
654 self.font_name.set(font.lower())
655
656 def set_samples(self, event=None):
657 """Update update both screen samples with the font settings.
658
659 Called on font initialization and change events.
660 Accesses font_name, font_size, and font_bold Variables.
Leo Ariasc3d95082018-02-03 18:36:10 -0600661 Updates font_sample and highlight page highlight_sample.
csabella9397e2a2017-07-30 13:34:25 -0400662 """
663 font_name = self.font_name.get()
664 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
665 new_font = (font_name, self.font_size.get(), font_weight)
666 self.font_sample['font'] = new_font
667 self.highlight_sample['font'] = new_font
668
669 def load_tab_cfg(self):
670 """Load current configuration settings for the tab options.
671
672 Attributes updated:
673 space_num: Set to value from idleConf.
674 """
675 # Set indent sizes.
676 space_num = idleConf.GetOption(
677 'main', 'Indent', 'num-spaces', default=4, type='int')
678 self.space_num.set(space_num)
679
680 def var_changed_space_num(self, *params):
681 "Store change to indentation size."
682 value = self.space_num.get()
683 changes.add_option('main', 'Indent', 'num-spaces', value)
684
685
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400686class HighPage(Frame):
687
688 def __init__(self, master):
689 super().__init__(master)
690 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400691 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400692 self.create_page_highlight()
693 self.load_theme_cfg()
694
695 def create_page_highlight(self):
696 """Return frame of widgets for Highlighting tab.
697
698 Enable users to provisionally change foreground and background
699 colors applied to textual tags. Color mappings are stored in
700 complete listings called themes. Built-in themes in
701 idlelib/config-highlight.def are fixed as far as the dialog is
702 concerned. Any theme can be used as the base for a new custom
703 theme, stored in .idlerc/config-highlight.cfg.
704
705 Function load_theme_cfg() initializes tk variables and theme
706 lists and calls paint_theme_sample() and set_highlight_target()
707 for the current theme. Radiobuttons builtin_theme_on and
708 custom_theme_on toggle var theme_source, which controls if the
709 current set of colors are from a builtin or custom theme.
710 DynOptionMenus builtinlist and customlist contain lists of the
711 builtin and custom themes, respectively, and the current item
712 from each list is stored in vars builtin_name and custom_name.
713
714 Function paint_theme_sample() applies the colors from the theme
715 to the tags in text widget highlight_sample and then invokes
716 set_color_sample(). Function set_highlight_target() sets the state
717 of the radiobuttons fg_on and bg_on based on the tag and it also
718 invokes set_color_sample().
719
720 Function set_color_sample() sets the background color for the frame
721 holding the color selector. This provides a larger visual of the
722 color for the current tag and plane (foreground/background).
723
724 Note: set_color_sample() is called from many places and is often
725 called more than once when a change is made. It is invoked when
726 foreground or background is selected (radiobuttons), from
727 paint_theme_sample() (theme is changed or load_cfg is called), and
728 from set_highlight_target() (target tag is changed or load_cfg called).
729
730 Button delete_custom invokes delete_custom() to delete
731 a custom theme from idleConf.userCfg['highlight'] and changes.
732 Button save_custom invokes save_as_new_theme() which calls
733 get_new_theme_name() and create_new() to save a custom theme
734 and its colors to idleConf.userCfg['highlight'].
735
736 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
737 if the current selected color for a tag is for the foreground or
738 background.
739
740 DynOptionMenu targetlist contains a readable description of the
741 tags applied to Python source within IDLE. Selecting one of the
742 tags from this list populates highlight_target, which has a callback
743 function set_highlight_target().
744
745 Text widget highlight_sample displays a block of text (which is
746 mock Python code) in which is embedded the defined tags and reflects
747 the color attributes of the current theme and changes for those tags.
748 Mouse button 1 allows for selection of a tag and updates
749 highlight_target with that tag value.
750
751 Note: The font in highlight_sample is set through the config in
752 the fonts tab.
753
754 In other words, a tag can be selected either from targetlist or
755 by clicking on the sample text within highlight_sample. The
756 plane (foreground/background) is selected via the radiobutton.
757 Together, these two (tag and plane) control what color is
758 shown in set_color_sample() for the current theme. Button set_color
759 invokes get_color() which displays a ColorChooser to change the
760 color for the selected tag/plane. If a new color is picked,
761 it will be saved to changes and the highlight_sample and
762 frame background will be updated.
763
764 Tk Variables:
765 color: Color of selected target.
766 builtin_name: Menu variable for built-in theme.
767 custom_name: Menu variable for custom theme.
768 fg_bg_toggle: Toggle for foreground/background color.
769 Note: this has no callback.
770 theme_source: Selector for built-in or custom theme.
771 highlight_target: Menu variable for the highlight tag target.
772
773 Instance Data Attributes:
774 theme_elements: Dictionary of tags for text highlighting.
775 The key is the display name and the value is a tuple of
776 (tag name, display sort order).
777
778 Methods [attachment]:
779 load_theme_cfg: Load current highlight colors.
780 get_color: Invoke colorchooser [button_set_color].
781 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
782 set_highlight_target: set fg_bg_toggle, set_color_sample().
783 set_color_sample: Set frame background to target.
784 on_new_color_set: Set new color and add option.
785 paint_theme_sample: Recolor sample.
786 get_new_theme_name: Get from popup.
787 create_new: Combine theme with changes and save.
788 save_as_new_theme: Save [button_save_custom].
789 set_theme_type: Command for [theme_source].
790 delete_custom: Activate default [button_delete_custom].
791 save_new: Save to userCfg['theme'] (is function).
792
793 Widgets of highlights page frame: (*) widgets bound to self
794 frame_custom: LabelFrame
795 (*)highlight_sample: Text
796 (*)frame_color_set: Frame
797 (*)button_set_color: Button
798 (*)targetlist: DynOptionMenu - highlight_target
799 frame_fg_bg_toggle: Frame
800 (*)fg_on: Radiobutton - fg_bg_toggle
801 (*)bg_on: Radiobutton - fg_bg_toggle
802 (*)button_save_custom: Button
803 frame_theme: LabelFrame
804 theme_type_title: Label
805 (*)builtin_theme_on: Radiobutton - theme_source
806 (*)custom_theme_on: Radiobutton - theme_source
807 (*)builtinlist: DynOptionMenu - builtin_name
808 (*)customlist: DynOptionMenu - custom_name
809 (*)button_delete_custom: Button
810 (*)theme_message: Label
811 """
812 self.theme_elements = {
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400813 'Normal Code or Text': ('normal', '00'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400814 'Code Context': ('context', '01'),
815 'Python Keywords': ('keyword', '02'),
816 'Python Definitions': ('definition', '03'),
817 'Python Builtins': ('builtin', '04'),
818 'Python Comments': ('comment', '05'),
819 'Python Strings': ('string', '06'),
820 'Selected Text': ('hilite', '07'),
821 'Found Text': ('hit', '08'),
822 'Cursor': ('cursor', '09'),
823 'Editor Breakpoint': ('break', '10'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400824 'Shell Prompt': ('console', '11'),
825 'Error Text': ('error', '12'),
826 'Shell User Output': ('stdout', '13'),
827 'Shell User Exception': ('stderr', '14'),
Tal Einat7123ea02019-07-23 15:22:11 +0300828 'Line Number': ('linenumber', '16'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400829 }
830 self.builtin_name = tracers.add(
831 StringVar(self), self.var_changed_builtin_name)
832 self.custom_name = tracers.add(
833 StringVar(self), self.var_changed_custom_name)
834 self.fg_bg_toggle = BooleanVar(self)
835 self.color = tracers.add(
836 StringVar(self), self.var_changed_color)
837 self.theme_source = tracers.add(
838 BooleanVar(self), self.var_changed_theme_source)
839 self.highlight_target = tracers.add(
840 StringVar(self), self.var_changed_highlight_target)
841
842 # Create widgets:
843 # body frame and section frames.
844 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
845 text=' Custom Highlighting ')
846 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
847 text=' Highlighting Theme ')
848 # frame_custom.
Tal Einat3221a632019-07-27 19:57:48 +0300849 sample_frame = ScrollableTextFrame(
850 frame_custom, relief=SOLID, borderwidth=1)
851 text = self.highlight_sample = sample_frame.text
852 text.configure(
853 font=('courier', 12, ''), cursor='hand2', width=1, height=1,
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400854 takefocus=FALSE, highlightthickness=0, wrap=NONE)
Cheryl Sabelladd023ad2020-01-27 17:15:56 -0500855 # Prevent perhaps invisible selection of word or slice.
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400856 text.bind('<Double-Button-1>', lambda e: 'break')
857 text.bind('<B1-Motion>', lambda e: 'break')
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400858 string_tags=(
859 ('# Click selects item.', 'comment'), ('\n', 'normal'),
860 ('code context section', 'context'), ('\n', 'normal'),
861 ('| cursor', 'cursor'), ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400862 ('def', 'keyword'), (' ', 'normal'),
863 ('func', 'definition'), ('(param):\n ', 'normal'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400864 ('"Return None."', 'string'), ('\n var0 = ', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400865 ("'string'", 'string'), ('\n var1 = ', 'normal'),
866 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
867 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
868 ('list', 'builtin'), ('(', 'normal'),
869 ('None', 'keyword'), (')\n', 'normal'),
870 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400871 ('>>>', 'console'), (' 3.14**2\n', 'normal'),
872 ('9.8596', 'stdout'), ('\n', 'normal'),
873 ('>>>', 'console'), (' pri ', 'normal'),
874 ('n', 'error'), ('t(\n', 'normal'),
875 ('SyntaxError', 'stderr'), ('\n', 'normal'))
876 for string, tag in string_tags:
877 text.insert(END, string, tag)
Tal Einat7123ea02019-07-23 15:22:11 +0300878 n_lines = len(text.get('1.0', END).splitlines())
Tal Einat3221a632019-07-27 19:57:48 +0300879 for lineno in range(1, n_lines):
Tal Einat7123ea02019-07-23 15:22:11 +0300880 text.insert(f'{lineno}.0',
881 f'{lineno:{len(str(n_lines))}d} ',
882 'linenumber')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400883 for element in self.theme_elements:
884 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400885 # event.widget.winfo_top_level().highlight_target.set(elem)
886 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400887 text.tag_bind(
888 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400889 text['state'] = 'disabled'
890 self.style.configure('frame_color_set.TFrame', borderwidth=1,
891 relief='solid')
892 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400893 frame_fg_bg_toggle = Frame(frame_custom)
894 self.button_set_color = Button(
895 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400896 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400897 self.targetlist = DynOptionMenu(
898 self.frame_color_set, self.highlight_target, None,
899 highlightthickness=0) #, command=self.set_highlight_targetBinding
900 self.fg_on = Radiobutton(
901 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
902 text='Foreground', command=self.set_color_sample_binding)
903 self.bg_on = Radiobutton(
904 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
905 text='Background', command=self.set_color_sample_binding)
906 self.fg_bg_toggle.set(1)
907 self.button_save_custom = Button(
908 frame_custom, text='Save as New Custom Theme',
909 command=self.save_as_new_theme)
910 # frame_theme.
911 theme_type_title = Label(frame_theme, text='Select : ')
912 self.builtin_theme_on = Radiobutton(
913 frame_theme, variable=self.theme_source, value=1,
914 command=self.set_theme_type, text='a Built-in Theme')
915 self.custom_theme_on = Radiobutton(
916 frame_theme, variable=self.theme_source, value=0,
917 command=self.set_theme_type, text='a Custom Theme')
918 self.builtinlist = DynOptionMenu(
919 frame_theme, self.builtin_name, None, command=None)
920 self.customlist = DynOptionMenu(
921 frame_theme, self.custom_name, None, command=None)
922 self.button_delete_custom = Button(
923 frame_theme, text='Delete Custom Theme',
924 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400925 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400926 # Pack widgets:
927 # body.
928 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500929 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400930 # frame_custom.
Tal Einat3221a632019-07-27 19:57:48 +0300931 self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400932 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
Tal Einat3221a632019-07-27 19:57:48 +0300933 sample_frame.pack(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400934 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
935 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
936 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
937 self.fg_on.pack(side=LEFT, anchor=E)
938 self.bg_on.pack(side=RIGHT, anchor=W)
939 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
940 # frame_theme.
941 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
942 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
943 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
944 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
945 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
946 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
947 self.theme_message.pack(side=TOP, fill=X, pady=5)
948
949 def load_theme_cfg(self):
950 """Load current configuration settings for the theme options.
951
952 Based on the theme_source toggle, the theme is set as
953 either builtin or custom and the initial widget values
954 reflect the current settings from idleConf.
955
956 Attributes updated:
957 theme_source: Set from idleConf.
958 builtinlist: List of default themes from idleConf.
959 customlist: List of custom themes from idleConf.
960 custom_theme_on: Disabled if there are no custom themes.
961 custom_theme: Message with additional information.
962 targetlist: Create menu from self.theme_elements.
963
964 Methods:
965 set_theme_type
966 paint_theme_sample
967 set_highlight_target
968 """
969 # Set current theme type radiobutton.
970 self.theme_source.set(idleConf.GetOption(
971 'main', 'Theme', 'default', type='bool', default=1))
972 # Set current theme.
973 current_option = idleConf.CurrentTheme()
974 # Load available theme option menus.
975 if self.theme_source.get(): # Default theme selected.
976 item_list = idleConf.GetSectionList('default', 'highlight')
977 item_list.sort()
978 self.builtinlist.SetMenu(item_list, current_option)
979 item_list = idleConf.GetSectionList('user', 'highlight')
980 item_list.sort()
981 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400982 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400983 self.custom_name.set('- no custom themes -')
984 else:
985 self.customlist.SetMenu(item_list, item_list[0])
986 else: # User theme selected.
987 item_list = idleConf.GetSectionList('user', 'highlight')
988 item_list.sort()
989 self.customlist.SetMenu(item_list, current_option)
990 item_list = idleConf.GetSectionList('default', 'highlight')
991 item_list.sort()
992 self.builtinlist.SetMenu(item_list, item_list[0])
993 self.set_theme_type()
994 # Load theme element option menu.
995 theme_names = list(self.theme_elements.keys())
996 theme_names.sort(key=lambda x: self.theme_elements[x][1])
997 self.targetlist.SetMenu(theme_names, theme_names[0])
998 self.paint_theme_sample()
999 self.set_highlight_target()
1000
1001 def var_changed_builtin_name(self, *params):
1002 """Process new builtin theme selection.
1003
1004 Add the changed theme's name to the changed_items and recreate
1005 the sample with the values from the selected theme.
1006 """
1007 old_themes = ('IDLE Classic', 'IDLE New')
1008 value = self.builtin_name.get()
1009 if value not in old_themes:
1010 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
1011 changes.add_option('main', 'Theme', 'name', old_themes[0])
1012 changes.add_option('main', 'Theme', 'name2', value)
1013 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001014 else:
1015 changes.add_option('main', 'Theme', 'name', value)
1016 changes.add_option('main', 'Theme', 'name2', '')
1017 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001018 self.paint_theme_sample()
1019
1020 def var_changed_custom_name(self, *params):
1021 """Process new custom theme selection.
1022
1023 If a new custom theme is selected, add the name to the
1024 changed_items and apply the theme to the sample.
1025 """
1026 value = self.custom_name.get()
1027 if value != '- no custom themes -':
1028 changes.add_option('main', 'Theme', 'name', value)
1029 self.paint_theme_sample()
1030
1031 def var_changed_theme_source(self, *params):
1032 """Process toggle between builtin and custom theme.
1033
1034 Update the default toggle value and apply the newly
1035 selected theme type.
1036 """
1037 value = self.theme_source.get()
1038 changes.add_option('main', 'Theme', 'default', value)
1039 if value:
1040 self.var_changed_builtin_name()
1041 else:
1042 self.var_changed_custom_name()
1043
1044 def var_changed_color(self, *params):
1045 "Process change to color choice."
1046 self.on_new_color_set()
1047
1048 def var_changed_highlight_target(self, *params):
1049 "Process selection of new target tag for highlighting."
1050 self.set_highlight_target()
1051
1052 def set_theme_type(self):
1053 """Set available screen options based on builtin or custom theme.
1054
1055 Attributes accessed:
1056 theme_source
1057
1058 Attributes updated:
1059 builtinlist
1060 customlist
1061 button_delete_custom
1062 custom_theme_on
1063
1064 Called from:
1065 handler for builtin_theme_on and custom_theme_on
1066 delete_custom
1067 create_new
1068 load_theme_cfg
1069 """
1070 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001071 self.builtinlist['state'] = 'normal'
1072 self.customlist['state'] = 'disabled'
1073 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001074 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001075 self.builtinlist['state'] = 'disabled'
1076 self.custom_theme_on.state(('!disabled',))
1077 self.customlist['state'] = 'normal'
1078 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001079
1080 def get_color(self):
1081 """Handle button to select a new color for the target tag.
1082
1083 If a new color is selected while using a builtin theme, a
1084 name must be supplied to create a custom theme.
1085
1086 Attributes accessed:
1087 highlight_target
1088 frame_color_set
1089 theme_source
1090
1091 Attributes updated:
1092 color
1093
1094 Methods:
1095 get_new_theme_name
1096 create_new
1097 """
1098 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001099 prev_color = self.style.lookup(self.frame_color_set['style'],
1100 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001101 rgbTuplet, color_string = tkColorChooser.askcolor(
1102 parent=self, title='Pick new color for : '+target,
1103 initialcolor=prev_color)
1104 if color_string and (color_string != prev_color):
1105 # User didn't cancel and they chose a new color.
1106 if self.theme_source.get(): # Current theme is a built-in.
1107 message = ('Your changes will be saved as a new Custom Theme. '
1108 'Enter a name for your new Custom Theme below.')
1109 new_theme = self.get_new_theme_name(message)
1110 if not new_theme: # User cancelled custom theme creation.
1111 return
1112 else: # Create new custom theme based on previously active theme.
1113 self.create_new(new_theme)
1114 self.color.set(color_string)
1115 else: # Current theme is user defined.
1116 self.color.set(color_string)
1117
1118 def on_new_color_set(self):
1119 "Display sample of new color selection on the dialog."
1120 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001121 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001122 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1123 sample_element = self.theme_elements[self.highlight_target.get()][0]
1124 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1125 theme = self.custom_name.get()
1126 theme_element = sample_element + '-' + plane
1127 changes.add_option('highlight', theme, theme_element, new_color)
1128
1129 def get_new_theme_name(self, message):
1130 "Return name of new theme from query popup."
1131 used_names = (idleConf.GetSectionList('user', 'highlight') +
1132 idleConf.GetSectionList('default', 'highlight'))
1133 new_theme = SectionName(
1134 self, 'New Custom Theme', message, used_names).result
1135 return new_theme
1136
1137 def save_as_new_theme(self):
1138 """Prompt for new theme name and create the theme.
1139
1140 Methods:
1141 get_new_theme_name
1142 create_new
1143 """
1144 new_theme_name = self.get_new_theme_name('New Theme Name:')
1145 if new_theme_name:
1146 self.create_new(new_theme_name)
1147
1148 def create_new(self, new_theme_name):
1149 """Create a new custom theme with the given name.
1150
1151 Create the new theme based on the previously active theme
1152 with the current changes applied. Once it is saved, then
1153 activate the new theme.
1154
1155 Attributes accessed:
1156 builtin_name
1157 custom_name
1158
1159 Attributes updated:
1160 customlist
1161 theme_source
1162
1163 Method:
1164 save_new
1165 set_theme_type
1166 """
1167 if self.theme_source.get():
1168 theme_type = 'default'
1169 theme_name = self.builtin_name.get()
1170 else:
1171 theme_type = 'user'
1172 theme_name = self.custom_name.get()
1173 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1174 # Apply any of the old theme's unsaved changes to the new theme.
1175 if theme_name in changes['highlight']:
1176 theme_changes = changes['highlight'][theme_name]
1177 for element in theme_changes:
1178 new_theme[element] = theme_changes[element]
1179 # Save the new theme.
1180 self.save_new(new_theme_name, new_theme)
1181 # Change GUI over to the new theme.
1182 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1183 custom_theme_list.sort()
1184 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1185 self.theme_source.set(0)
1186 self.set_theme_type()
1187
1188 def set_highlight_target(self):
1189 """Set fg/bg toggle and color based on highlight tag target.
1190
1191 Instance variables accessed:
1192 highlight_target
1193
1194 Attributes updated:
1195 fg_on
1196 bg_on
1197 fg_bg_toggle
1198
1199 Methods:
1200 set_color_sample
1201
1202 Called from:
1203 var_changed_highlight_target
1204 load_theme_cfg
1205 """
1206 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001207 self.fg_on.state(('disabled',))
1208 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001209 self.fg_bg_toggle.set(1)
1210 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001211 self.fg_on.state(('!disabled',))
1212 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001213 self.fg_bg_toggle.set(1)
1214 self.set_color_sample()
1215
1216 def set_color_sample_binding(self, *args):
1217 """Change color sample based on foreground/background toggle.
1218
1219 Methods:
1220 set_color_sample
1221 """
1222 self.set_color_sample()
1223
1224 def set_color_sample(self):
1225 """Set the color of the frame background to reflect the selected target.
1226
1227 Instance variables accessed:
1228 theme_elements
1229 highlight_target
1230 fg_bg_toggle
1231 highlight_sample
1232
1233 Attributes updated:
1234 frame_color_set
1235 """
1236 # Set the color sample area.
1237 tag = self.theme_elements[self.highlight_target.get()][0]
1238 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1239 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001240 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001241
1242 def paint_theme_sample(self):
1243 """Apply the theme colors to each element tag in the sample text.
1244
1245 Instance attributes accessed:
1246 theme_elements
1247 theme_source
1248 builtin_name
1249 custom_name
1250
1251 Attributes updated:
1252 highlight_sample: Set the tag elements to the theme.
1253
1254 Methods:
1255 set_color_sample
1256
1257 Called from:
1258 var_changed_builtin_name
1259 var_changed_custom_name
1260 load_theme_cfg
1261 """
1262 if self.theme_source.get(): # Default theme
1263 theme = self.builtin_name.get()
1264 else: # User theme
1265 theme = self.custom_name.get()
1266 for element_title in self.theme_elements:
1267 element = self.theme_elements[element_title][0]
1268 colors = idleConf.GetHighlight(theme, element)
1269 if element == 'cursor': # Cursor sample needs special painting.
1270 colors['background'] = idleConf.GetHighlight(
Terry Jan Reedyc1419572019-03-22 18:23:41 -04001271 theme, 'normal')['background']
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001272 # Handle any unsaved changes to this theme.
1273 if theme in changes['highlight']:
1274 theme_dict = changes['highlight'][theme]
1275 if element + '-foreground' in theme_dict:
1276 colors['foreground'] = theme_dict[element + '-foreground']
1277 if element + '-background' in theme_dict:
1278 colors['background'] = theme_dict[element + '-background']
1279 self.highlight_sample.tag_config(element, **colors)
1280 self.set_color_sample()
1281
1282 def save_new(self, theme_name, theme):
1283 """Save a newly created theme to idleConf.
1284
1285 theme_name - string, the name of the new theme
1286 theme - dictionary containing the new theme
1287 """
Cheryl Sabelladd023ad2020-01-27 17:15:56 -05001288 idleConf.userCfg['highlight'].AddSection(theme_name)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001289 for element in theme:
1290 value = theme[element]
1291 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1292
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001293 def askyesno(self, *args, **kwargs):
1294 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001295 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001296
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001297 def delete_custom(self):
1298 """Handle event to delete custom theme.
1299
1300 The current theme is deactivated and the default theme is
1301 activated. The custom theme is permanently removed from
1302 the config file.
1303
1304 Attributes accessed:
1305 custom_name
1306
1307 Attributes updated:
1308 custom_theme_on
1309 customlist
1310 theme_source
1311 builtin_name
1312
1313 Methods:
1314 deactivate_current_config
1315 save_all_changed_extensions
1316 activate_config_changes
1317 set_theme_type
1318 """
1319 theme_name = self.custom_name.get()
1320 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001321 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001322 'Delete Theme', delmsg % theme_name, parent=self):
1323 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001324 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001325 # Remove theme from changes, config, and file.
1326 changes.delete_section('highlight', theme_name)
1327 # Reload user theme list.
1328 item_list = idleConf.GetSectionList('user', 'highlight')
1329 item_list.sort()
1330 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001331 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001332 self.customlist.SetMenu(item_list, '- no custom themes -')
1333 else:
1334 self.customlist.SetMenu(item_list, item_list[0])
1335 # Revert to default theme.
1336 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1337 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1338 # User can't back out of these changes, they must be applied now.
1339 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001340 self.cd.save_all_changed_extensions()
1341 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001342 self.set_theme_type()
1343
1344
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001345class KeysPage(Frame):
1346
1347 def __init__(self, master):
1348 super().__init__(master)
1349 self.cd = master.master
1350 self.create_page_keys()
1351 self.load_key_cfg()
1352
1353 def create_page_keys(self):
1354 """Return frame of widgets for Keys tab.
1355
1356 Enable users to provisionally change both individual and sets of
1357 keybindings (shortcut keys). Except for features implemented as
1358 extensions, keybindings are stored in complete sets called
1359 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1360 as far as the dialog is concerned. Any keyset can be used as the
1361 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1362
1363 Function load_key_cfg() initializes tk variables and keyset
1364 lists and calls load_keys_list for the current keyset.
1365 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1366 keyset_source, which controls if the current set of keybindings
1367 are from a builtin or custom keyset. DynOptionMenus builtinlist
1368 and customlist contain lists of the builtin and custom keysets,
1369 respectively, and the current item from each list is stored in
1370 vars builtin_name and custom_name.
1371
1372 Button delete_custom_keys invokes delete_custom_keys() to delete
1373 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1374 save_custom_keys invokes save_as_new_key_set() which calls
1375 get_new_keys_name() and create_new_key_set() to save a custom keyset
1376 and its keybindings to idleConf.userCfg['keys'].
1377
1378 Listbox bindingslist contains all of the keybindings for the
1379 selected keyset. The keybindings are loaded in load_keys_list()
1380 and are pairs of (event, [keys]) where keys can be a list
1381 of one or more key combinations to bind to the same event.
1382 Mouse button 1 click invokes on_bindingslist_select(), which
1383 allows button_new_keys to be clicked.
1384
1385 So, an item is selected in listbindings, which activates
1386 button_new_keys, and clicking button_new_keys calls function
1387 get_new_keys(). Function get_new_keys() gets the key mappings from the
1388 current keyset for the binding event item that was selected. The
1389 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001390 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001391 to be entered for that binding event. If the keys aren't
1392 changed, nothing happens. If the keys are changed and the keyset
1393 is a builtin, function get_new_keys_name() will be called
1394 for input of a custom keyset name. If no name is given, then the
1395 change to the keybinding will abort and no updates will be made. If
1396 a custom name is entered in the prompt or if the current keyset was
1397 already custom (and thus didn't require a prompt), then
1398 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1399 with the change to the event binding. The item listing in bindingslist
1400 is updated with the new keys. Var keybinding is also set which invokes
1401 the callback function, var_changed_keybinding, to add the change to
1402 the 'keys' or 'extensions' changes tracker based on the binding type.
1403
1404 Tk Variables:
1405 keybinding: Action/key bindings.
1406
1407 Methods:
1408 load_keys_list: Reload active set.
1409 create_new_key_set: Combine active keyset and changes.
1410 set_keys_type: Command for keyset_source.
1411 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1412 deactivate_current_config: Remove keys bindings in editors.
1413
1414 Widgets for KeysPage(frame): (*) widgets bound to self
1415 frame_key_sets: LabelFrame
1416 frames[0]: Frame
1417 (*)builtin_keyset_on: Radiobutton - var keyset_source
1418 (*)custom_keyset_on: Radiobutton - var keyset_source
1419 (*)builtinlist: DynOptionMenu - var builtin_name,
1420 func keybinding_selected
1421 (*)customlist: DynOptionMenu - var custom_name,
1422 func keybinding_selected
1423 (*)keys_message: Label
1424 frames[1]: Frame
1425 (*)button_delete_custom_keys: Button - delete_custom_keys
1426 (*)button_save_custom_keys: Button - save_as_new_key_set
1427 frame_custom: LabelFrame
1428 frame_target: Frame
1429 target_title: Label
1430 scroll_target_y: Scrollbar
1431 scroll_target_x: Scrollbar
1432 (*)bindingslist: ListBox - on_bindingslist_select
1433 (*)button_new_keys: Button - get_new_keys & ..._name
1434 """
1435 self.builtin_name = tracers.add(
1436 StringVar(self), self.var_changed_builtin_name)
1437 self.custom_name = tracers.add(
1438 StringVar(self), self.var_changed_custom_name)
1439 self.keyset_source = tracers.add(
1440 BooleanVar(self), self.var_changed_keyset_source)
1441 self.keybinding = tracers.add(
1442 StringVar(self), self.var_changed_keybinding)
1443
1444 # Create widgets:
1445 # body and section frames.
1446 frame_custom = LabelFrame(
1447 self, borderwidth=2, relief=GROOVE,
1448 text=' Custom Key Bindings ')
1449 frame_key_sets = LabelFrame(
1450 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1451 # frame_custom.
1452 frame_target = Frame(frame_custom)
1453 target_title = Label(frame_target, text='Action - Key(s)')
1454 scroll_target_y = Scrollbar(frame_target)
1455 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1456 self.bindingslist = Listbox(
1457 frame_target, takefocus=FALSE, exportselection=FALSE)
1458 self.bindingslist.bind('<ButtonRelease-1>',
1459 self.on_bindingslist_select)
1460 scroll_target_y['command'] = self.bindingslist.yview
1461 scroll_target_x['command'] = self.bindingslist.xview
1462 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1463 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1464 self.button_new_keys = Button(
1465 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001466 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001467 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001468 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001469 for i in range(2)]
1470 self.builtin_keyset_on = Radiobutton(
1471 frames[0], variable=self.keyset_source, value=1,
1472 command=self.set_keys_type, text='Use a Built-in Key Set')
1473 self.custom_keyset_on = Radiobutton(
1474 frames[0], variable=self.keyset_source, value=0,
1475 command=self.set_keys_type, text='Use a Custom Key Set')
1476 self.builtinlist = DynOptionMenu(
1477 frames[0], self.builtin_name, None, command=None)
1478 self.customlist = DynOptionMenu(
1479 frames[0], self.custom_name, None, command=None)
1480 self.button_delete_custom_keys = Button(
1481 frames[1], text='Delete Custom Key Set',
1482 command=self.delete_custom_keys)
1483 self.button_save_custom_keys = Button(
1484 frames[1], text='Save as New Custom Key Set',
1485 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001486 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001487
1488 # Pack widgets:
1489 # body.
1490 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1491 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1492 # frame_custom.
1493 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1494 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1495 # frame_target.
1496 frame_target.columnconfigure(0, weight=1)
1497 frame_target.rowconfigure(1, weight=1)
1498 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1499 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1500 scroll_target_y.grid(row=1, column=1, sticky=NS)
1501 scroll_target_x.grid(row=2, column=0, sticky=EW)
1502 # frame_key_sets.
1503 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1504 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1505 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1506 self.customlist.grid(row=1, column=1, sticky=NSEW)
1507 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1508 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1509 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1510 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1511 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1512
1513 def load_key_cfg(self):
1514 "Load current configuration settings for the keybinding options."
1515 # Set current keys type radiobutton.
1516 self.keyset_source.set(idleConf.GetOption(
1517 'main', 'Keys', 'default', type='bool', default=1))
1518 # Set current keys.
1519 current_option = idleConf.CurrentKeys()
1520 # Load available keyset option menus.
1521 if self.keyset_source.get(): # Default theme selected.
1522 item_list = idleConf.GetSectionList('default', 'keys')
1523 item_list.sort()
1524 self.builtinlist.SetMenu(item_list, current_option)
1525 item_list = idleConf.GetSectionList('user', 'keys')
1526 item_list.sort()
1527 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001528 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001529 self.custom_name.set('- no custom keys -')
1530 else:
1531 self.customlist.SetMenu(item_list, item_list[0])
1532 else: # User key set selected.
1533 item_list = idleConf.GetSectionList('user', 'keys')
1534 item_list.sort()
1535 self.customlist.SetMenu(item_list, current_option)
1536 item_list = idleConf.GetSectionList('default', 'keys')
1537 item_list.sort()
1538 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1539 self.set_keys_type()
1540 # Load keyset element list.
1541 keyset_name = idleConf.CurrentKeys()
1542 self.load_keys_list(keyset_name)
1543
1544 def var_changed_builtin_name(self, *params):
1545 "Process selection of builtin key set."
1546 old_keys = (
1547 'IDLE Classic Windows',
1548 'IDLE Classic Unix',
1549 'IDLE Classic Mac',
1550 'IDLE Classic OSX',
1551 )
1552 value = self.builtin_name.get()
1553 if value not in old_keys:
1554 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1555 changes.add_option('main', 'Keys', 'name', old_keys[0])
1556 changes.add_option('main', 'Keys', 'name2', value)
1557 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001558 else:
1559 changes.add_option('main', 'Keys', 'name', value)
1560 changes.add_option('main', 'Keys', 'name2', '')
1561 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001562 self.load_keys_list(value)
1563
1564 def var_changed_custom_name(self, *params):
1565 "Process selection of custom key set."
1566 value = self.custom_name.get()
1567 if value != '- no custom keys -':
1568 changes.add_option('main', 'Keys', 'name', value)
1569 self.load_keys_list(value)
1570
1571 def var_changed_keyset_source(self, *params):
1572 "Process toggle between builtin key set and custom key set."
1573 value = self.keyset_source.get()
1574 changes.add_option('main', 'Keys', 'default', value)
1575 if value:
1576 self.var_changed_builtin_name()
1577 else:
1578 self.var_changed_custom_name()
1579
1580 def var_changed_keybinding(self, *params):
1581 "Store change to a keybinding."
1582 value = self.keybinding.get()
1583 key_set = self.custom_name.get()
1584 event = self.bindingslist.get(ANCHOR).split()[0]
1585 if idleConf.IsCoreBinding(event):
1586 changes.add_option('keys', key_set, event, value)
1587 else: # Event is an extension binding.
1588 ext_name = idleConf.GetExtnNameForEvent(event)
1589 ext_keybind_section = ext_name + '_cfgBindings'
1590 changes.add_option('extensions', ext_keybind_section, event, value)
1591
1592 def set_keys_type(self):
1593 "Set available screen options based on builtin or custom key set."
1594 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001595 self.builtinlist['state'] = 'normal'
1596 self.customlist['state'] = 'disabled'
1597 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001598 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001599 self.builtinlist['state'] = 'disabled'
1600 self.custom_keyset_on.state(('!disabled',))
1601 self.customlist['state'] = 'normal'
1602 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001603
1604 def get_new_keys(self):
1605 """Handle event to change key binding for selected line.
1606
1607 A selection of a key/binding in the list of current
1608 bindings pops up a dialog to enter a new binding. If
1609 the current key set is builtin and a binding has
1610 changed, then a name for a custom key set needs to be
1611 entered for the change to be applied.
1612 """
1613 list_index = self.bindingslist.index(ANCHOR)
1614 binding = self.bindingslist.get(list_index)
1615 bind_name = binding.split()[0]
1616 if self.keyset_source.get():
1617 current_key_set_name = self.builtin_name.get()
1618 else:
1619 current_key_set_name = self.custom_name.get()
1620 current_bindings = idleConf.GetCurrentKeySet()
1621 if current_key_set_name in changes['keys']: # unsaved changes
1622 key_set_changes = changes['keys'][current_key_set_name]
1623 for event in key_set_changes:
1624 current_bindings[event] = key_set_changes[event].split()
1625 current_key_sequences = list(current_bindings.values())
1626 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1627 current_key_sequences).result
1628 if new_keys:
1629 if self.keyset_source.get(): # Current key set is a built-in.
1630 message = ('Your changes will be saved as a new Custom Key Set.'
1631 ' Enter a name for your new Custom Key Set below.')
1632 new_keyset = self.get_new_keys_name(message)
1633 if not new_keyset: # User cancelled custom key set creation.
1634 self.bindingslist.select_set(list_index)
1635 self.bindingslist.select_anchor(list_index)
1636 return
1637 else: # Create new custom key set based on previously active key set.
1638 self.create_new_key_set(new_keyset)
1639 self.bindingslist.delete(list_index)
1640 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1641 self.bindingslist.select_set(list_index)
1642 self.bindingslist.select_anchor(list_index)
1643 self.keybinding.set(new_keys)
1644 else:
1645 self.bindingslist.select_set(list_index)
1646 self.bindingslist.select_anchor(list_index)
1647
1648 def get_new_keys_name(self, message):
1649 "Return new key set name from query popup."
1650 used_names = (idleConf.GetSectionList('user', 'keys') +
1651 idleConf.GetSectionList('default', 'keys'))
1652 new_keyset = SectionName(
1653 self, 'New Custom Key Set', message, used_names).result
1654 return new_keyset
1655
1656 def save_as_new_key_set(self):
1657 "Prompt for name of new key set and save changes using that name."
1658 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1659 if new_keys_name:
1660 self.create_new_key_set(new_keys_name)
1661
1662 def on_bindingslist_select(self, event):
1663 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001664 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001665
1666 def create_new_key_set(self, new_key_set_name):
1667 """Create a new custom key set with the given name.
1668
1669 Copy the bindings/keys from the previously active keyset
1670 to the new keyset and activate the new custom keyset.
1671 """
1672 if self.keyset_source.get():
1673 prev_key_set_name = self.builtin_name.get()
1674 else:
1675 prev_key_set_name = self.custom_name.get()
1676 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1677 new_keys = {}
1678 for event in prev_keys: # Add key set to changed items.
1679 event_name = event[2:-2] # Trim off the angle brackets.
1680 binding = ' '.join(prev_keys[event])
1681 new_keys[event_name] = binding
1682 # Handle any unsaved changes to prev key set.
1683 if prev_key_set_name in changes['keys']:
1684 key_set_changes = changes['keys'][prev_key_set_name]
1685 for event in key_set_changes:
1686 new_keys[event] = key_set_changes[event]
1687 # Save the new key set.
1688 self.save_new_key_set(new_key_set_name, new_keys)
1689 # Change GUI over to the new key set.
1690 custom_key_list = idleConf.GetSectionList('user', 'keys')
1691 custom_key_list.sort()
1692 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1693 self.keyset_source.set(0)
1694 self.set_keys_type()
1695
1696 def load_keys_list(self, keyset_name):
1697 """Reload the list of action/key binding pairs for the active key set.
1698
1699 An action/key binding can be selected to change the key binding.
1700 """
1701 reselect = False
1702 if self.bindingslist.curselection():
1703 reselect = True
1704 list_index = self.bindingslist.index(ANCHOR)
1705 keyset = idleConf.GetKeySet(keyset_name)
1706 bind_names = list(keyset.keys())
1707 bind_names.sort()
1708 self.bindingslist.delete(0, END)
1709 for bind_name in bind_names:
1710 key = ' '.join(keyset[bind_name])
1711 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1712 if keyset_name in changes['keys']:
1713 # Handle any unsaved changes to this key set.
1714 if bind_name in changes['keys'][keyset_name]:
1715 key = changes['keys'][keyset_name][bind_name]
1716 self.bindingslist.insert(END, bind_name+' - '+key)
1717 if reselect:
1718 self.bindingslist.see(list_index)
1719 self.bindingslist.select_set(list_index)
1720 self.bindingslist.select_anchor(list_index)
1721
1722 @staticmethod
1723 def save_new_key_set(keyset_name, keyset):
1724 """Save a newly created core key set.
1725
1726 Add keyset to idleConf.userCfg['keys'], not to disk.
1727 If the keyset doesn't exist, it is created. The
1728 binding/keys are taken from the keyset argument.
1729
1730 keyset_name - string, the name of the new key set
1731 keyset - dictionary containing the new keybindings
1732 """
Cheryl Sabelladd023ad2020-01-27 17:15:56 -05001733 idleConf.userCfg['keys'].AddSection(keyset_name)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001734 for event in keyset:
1735 value = keyset[event]
1736 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1737
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001738 def askyesno(self, *args, **kwargs):
1739 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001740 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001741
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001742 def delete_custom_keys(self):
1743 """Handle event to delete a custom key set.
1744
1745 Applying the delete deactivates the current configuration and
1746 reverts to the default. The custom key set is permanently
1747 deleted from the config file.
1748 """
1749 keyset_name = self.custom_name.get()
1750 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001751 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001752 'Delete Key Set', delmsg % keyset_name, parent=self):
1753 return
1754 self.cd.deactivate_current_config()
1755 # Remove key set from changes, config, and file.
1756 changes.delete_section('keys', keyset_name)
1757 # Reload user key set list.
1758 item_list = idleConf.GetSectionList('user', 'keys')
1759 item_list.sort()
1760 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001761 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001762 self.customlist.SetMenu(item_list, '- no custom keys -')
1763 else:
1764 self.customlist.SetMenu(item_list, item_list[0])
1765 # Revert to default key set.
1766 self.keyset_source.set(idleConf.defaultCfg['main']
Tal Einat604e7b92018-09-25 15:10:14 +03001767 .Get('Keys', 'default'))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001768 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
Tal Einat604e7b92018-09-25 15:10:14 +03001769 or idleConf.default_keys())
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001770 # User can't back out of these changes, they must be applied now.
1771 changes.save_all()
1772 self.cd.save_all_changed_extensions()
1773 self.cd.activate_config_changes()
1774 self.set_keys_type()
1775
1776
csabellae8eb17b2017-07-30 18:39:17 -04001777class GenPage(Frame):
1778
csabella6f446be2017-08-01 00:24:07 -04001779 def __init__(self, master):
1780 super().__init__(master)
Tal Einat1ebee372019-07-23 13:02:11 +03001781
1782 self.init_validators()
csabellae8eb17b2017-07-30 18:39:17 -04001783 self.create_page_general()
1784 self.load_general_cfg()
1785
Tal Einat1ebee372019-07-23 13:02:11 +03001786 def init_validators(self):
1787 digits_or_empty_re = re.compile(r'[0-9]*')
1788 def is_digits_or_empty(s):
1789 "Return 's is blank or contains only digits'"
1790 return digits_or_empty_re.fullmatch(s) is not None
1791 self.digits_only = (self.register(is_digits_or_empty), '%P',)
1792
csabellae8eb17b2017-07-30 18:39:17 -04001793 def create_page_general(self):
1794 """Return frame of widgets for General tab.
1795
1796 Enable users to provisionally change general options. Function
Terry Jan Reedy0acb6462019-07-30 18:14:58 -04001797 load_general_cfg initializes tk variables and helplist using
csabellae8eb17b2017-07-30 18:39:17 -04001798 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1799 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1800 set var autosave. Entry boxes win_width_int and win_height_int
1801 set var win_width and win_height. Setting var_name invokes the
1802 default callback that adds option to changes.
1803
1804 Helplist: load_general_cfg loads list user_helplist with
1805 name, position pairs and copies names to listbox helplist.
1806 Clicking a name invokes help_source selected. Clicking
1807 button_helplist_name invokes helplist_item_name, which also
1808 changes user_helplist. These functions all call
1809 set_add_delete_state. All but load call update_help_changes to
1810 rewrite changes['main']['HelpFiles'].
1811
Cheryl Sabella2f896462017-08-14 21:21:43 -04001812 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001813 frame_window: LabelFrame
1814 frame_run: Frame
1815 startup_title: Label
1816 (*)startup_editor_on: Radiobutton - startup_edit
1817 (*)startup_shell_on: Radiobutton - startup_edit
1818 frame_win_size: Frame
1819 win_size_title: Label
1820 win_width_title: Label
1821 (*)win_width_int: Entry - win_width
1822 win_height_title: Label
1823 (*)win_height_int: Entry - win_height
Zackery Spytz9c284492019-11-13 00:13:33 -07001824 frame_cursor_blink: Frame
1825 cursor_blink_title: Label
1826 (*)cursor_blink_bool: Checkbutton - cursor_blink
Cheryl Sabella845d8642018-02-04 18:15:21 -05001827 frame_autocomplete: Frame
1828 auto_wait_title: Label
1829 (*)auto_wait_int: Entry - autocomplete_wait
1830 frame_paren1: Frame
1831 paren_style_title: Label
1832 (*)paren_style_type: OptionMenu - paren_style
1833 frame_paren2: Frame
1834 paren_time_title: Label
1835 (*)paren_flash_time: Entry - flash_delay
1836 (*)bell_on: Checkbutton - paren_bell
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001837 frame_editor: LabelFrame
1838 frame_save: Frame
1839 run_save_title: Label
1840 (*)save_ask_on: Radiobutton - autosave
1841 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella845d8642018-02-04 18:15:21 -05001842 frame_format: Frame
1843 format_width_title: Label
1844 (*)format_width_int: Entry - format_width
Tal Einat7123ea02019-07-23 15:22:11 +03001845 frame_line_numbers_default: Frame
1846 line_numbers_default_title: Label
1847 (*)line_numbers_default_bool: Checkbutton - line_numbers_default
Cheryl Sabella845d8642018-02-04 18:15:21 -05001848 frame_context: Frame
1849 context_title: Label
1850 (*)context_int: Entry - context_lines
Tal Einat604e7b92018-09-25 15:10:14 +03001851 frame_shell: LabelFrame
1852 frame_auto_squeeze_min_lines: Frame
1853 auto_squeeze_min_lines_title: Label
1854 (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines
Cheryl Sabella2f896462017-08-14 21:21:43 -04001855 frame_help: LabelFrame
1856 frame_helplist: Frame
1857 frame_helplist_buttons: Frame
1858 (*)button_helplist_edit
1859 (*)button_helplist_add
1860 (*)button_helplist_remove
1861 (*)helplist: ListBox
1862 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001863 """
wohlganger58fc71c2017-09-10 16:19:47 -05001864 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001865 self.startup_edit = tracers.add(
1866 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001867 self.win_width = tracers.add(
1868 StringVar(self), ('main', 'EditorWindow', 'width'))
1869 self.win_height = tracers.add(
1870 StringVar(self), ('main', 'EditorWindow', 'height'))
Zackery Spytz9c284492019-11-13 00:13:33 -07001871 self.cursor_blink = tracers.add(
1872 BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink'))
wohlganger58fc71c2017-09-10 16:19:47 -05001873 self.autocomplete_wait = tracers.add(
1874 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1875 self.paren_style = tracers.add(
1876 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1877 self.flash_delay = tracers.add(
1878 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1879 self.paren_bell = tracers.add(
1880 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001881
Tal Einat604e7b92018-09-25 15:10:14 +03001882 self.auto_squeeze_min_lines = tracers.add(
1883 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
1884
wohlganger58fc71c2017-09-10 16:19:47 -05001885 self.autosave = tracers.add(
1886 IntVar(self), ('main', 'General', 'autosave'))
1887 self.format_width = tracers.add(
1888 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
Tal Einat7123ea02019-07-23 15:22:11 +03001889 self.line_numbers_default = tracers.add(
1890 BooleanVar(self),
1891 ('main', 'EditorWindow', 'line-numbers-default'))
wohlganger58fc71c2017-09-10 16:19:47 -05001892 self.context_lines = tracers.add(
Cheryl Sabella29996a12018-06-01 19:23:00 -04001893 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
wohlganger58fc71c2017-09-10 16:19:47 -05001894
1895 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001896 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001897 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1898 text=' Window Preferences')
1899 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1900 text=' Editor Preferences')
Tal Einat604e7b92018-09-25 15:10:14 +03001901 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
1902 text=' Shell Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001903 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
Tal Einat604e7b92018-09-25 15:10:14 +03001904 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001905 # Frame_window.
1906 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001907 startup_title = Label(frame_run, text='At Startup')
1908 self.startup_editor_on = Radiobutton(
1909 frame_run, variable=self.startup_edit, value=1,
1910 text="Open Edit Window")
1911 self.startup_shell_on = Radiobutton(
1912 frame_run, variable=self.startup_edit, value=0,
1913 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001914
wohlganger58fc71c2017-09-10 16:19:47 -05001915 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001916 win_size_title = Label(
1917 frame_win_size, text='Initial Window Size (in characters)')
1918 win_width_title = Label(frame_win_size, text='Width')
1919 self.win_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001920 frame_win_size, textvariable=self.win_width, width=3,
1921 validatecommand=self.digits_only, validate='key',
1922 )
csabellae8eb17b2017-07-30 18:39:17 -04001923 win_height_title = Label(frame_win_size, text='Height')
1924 self.win_height_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001925 frame_win_size, textvariable=self.win_height, width=3,
1926 validatecommand=self.digits_only, validate='key',
1927 )
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001928
Zackery Spytz9c284492019-11-13 00:13:33 -07001929 frame_cursor_blink = Frame(frame_window, borderwidth=0)
1930 cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink')
1931 self.cursor_blink_bool = Checkbutton(frame_cursor_blink,
1932 variable=self.cursor_blink, width=1)
1933
wohlganger58fc71c2017-09-10 16:19:47 -05001934 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1935 auto_wait_title = Label(frame_autocomplete,
1936 text='Completions Popup Wait (milliseconds)')
1937 self.auto_wait_int = Entry(frame_autocomplete, width=6,
Tal Einat1ebee372019-07-23 13:02:11 +03001938 textvariable=self.autocomplete_wait,
1939 validatecommand=self.digits_only,
1940 validate='key',
1941 )
wohlganger58fc71c2017-09-10 16:19:47 -05001942
1943 frame_paren1 = Frame(frame_window, borderwidth=0)
1944 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1945 self.paren_style_type = OptionMenu(
1946 frame_paren1, self.paren_style, 'expression',
1947 "opener","parens","expression")
1948 frame_paren2 = Frame(frame_window, borderwidth=0)
1949 paren_time_title = Label(
1950 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1951 '(0 is until next input)')
1952 self.paren_flash_time = Entry(
1953 frame_paren2, textvariable=self.flash_delay, width=6)
1954 self.bell_on = Checkbutton(
1955 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1956
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001957 # Frame_editor.
1958 frame_save = Frame(frame_editor, borderwidth=0)
1959 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1960 self.save_ask_on = Radiobutton(
1961 frame_save, variable=self.autosave, value=0,
1962 text="Prompt to Save")
1963 self.save_auto_on = Radiobutton(
1964 frame_save, variable=self.autosave, value=1,
1965 text='No Prompt')
1966
wohlganger58fc71c2017-09-10 16:19:47 -05001967 frame_format = Frame(frame_editor, borderwidth=0)
1968 format_width_title = Label(frame_format,
1969 text='Format Paragraph Max Width')
1970 self.format_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001971 frame_format, textvariable=self.format_width, width=4,
1972 validatecommand=self.digits_only, validate='key',
1973 )
wohlganger58fc71c2017-09-10 16:19:47 -05001974
Tal Einat7123ea02019-07-23 15:22:11 +03001975 frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
1976 line_numbers_default_title = Label(
1977 frame_line_numbers_default, text='Show line numbers in new windows')
1978 self.line_numbers_default_bool = Checkbutton(
1979 frame_line_numbers_default,
1980 variable=self.line_numbers_default,
1981 width=1)
1982
wohlganger58fc71c2017-09-10 16:19:47 -05001983 frame_context = Frame(frame_editor, borderwidth=0)
Cheryl Sabella29996a12018-06-01 19:23:00 -04001984 context_title = Label(frame_context, text='Max Context Lines :')
wohlganger58fc71c2017-09-10 16:19:47 -05001985 self.context_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001986 frame_context, textvariable=self.context_lines, width=3,
1987 validatecommand=self.digits_only, validate='key',
1988 )
wohlganger58fc71c2017-09-10 16:19:47 -05001989
Tal Einat604e7b92018-09-25 15:10:14 +03001990 # Frame_shell.
1991 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
1992 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
1993 text='Auto-Squeeze Min. Lines:')
1994 self.auto_squeeze_min_lines_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001995 frame_auto_squeeze_min_lines, width=4,
1996 textvariable=self.auto_squeeze_min_lines,
1997 validatecommand=self.digits_only, validate='key',
1998 )
wohlganger58fc71c2017-09-10 16:19:47 -05001999
csabellae8eb17b2017-07-30 18:39:17 -04002000 # frame_help.
2001 frame_helplist = Frame(frame_help)
2002 frame_helplist_buttons = Frame(frame_helplist)
2003 self.helplist = Listbox(
2004 frame_helplist, height=5, takefocus=True,
2005 exportselection=FALSE)
2006 scroll_helplist = Scrollbar(frame_helplist)
2007 scroll_helplist['command'] = self.helplist.yview
2008 self.helplist['yscrollcommand'] = scroll_helplist.set
2009 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
2010 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04002011 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04002012 width=8, command=self.helplist_item_edit)
2013 self.button_helplist_add = Button(
2014 frame_helplist_buttons, text='Add',
2015 width=8, command=self.helplist_item_add)
2016 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04002017 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04002018 width=8, command=self.helplist_item_remove)
2019
2020 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002021 # Body.
2022 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2023 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Tal Einat604e7b92018-09-25 15:10:14 +03002024 frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04002025 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2026 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002027 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002028 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2029 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2030 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04002031 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002032 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002033 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2034 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2035 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
2036 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2037 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
Zackery Spytz9c284492019-11-13 00:13:33 -07002038 # frame_cursor_blink.
2039 frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X)
2040 cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2041 self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002042 # frame_autocomplete.
2043 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
2044 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2045 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
2046 # frame_paren.
2047 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
2048 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2049 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
2050 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
2051 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
2052 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
2053 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
2054
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002055 # frame_save.
2056 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
2057 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2058 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2059 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002060 # frame_format.
2061 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
2062 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2063 self.format_width_int.pack(side=TOP, padx=10, pady=5)
Tal Einat7123ea02019-07-23 15:22:11 +03002064 # frame_line_numbers_default.
2065 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
2066 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2067 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002068 # frame_context.
2069 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
2070 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2071 self.context_int.pack(side=TOP, padx=5, pady=5)
2072
Tal Einat604e7b92018-09-25 15:10:14 +03002073 # frame_auto_squeeze_min_lines
2074 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
2075 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2076 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
2077
csabellae8eb17b2017-07-30 18:39:17 -04002078 # frame_help.
2079 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
2080 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2081 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
2082 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
2083 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
2084 self.button_helplist_add.pack(side=TOP, anchor=W)
2085 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
2086
2087 def load_general_cfg(self):
2088 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05002089 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04002090 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05002091 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04002092 self.win_width.set(idleConf.GetOption(
2093 'main', 'EditorWindow', 'width', type='int'))
2094 self.win_height.set(idleConf.GetOption(
2095 'main', 'EditorWindow', 'height', type='int'))
Zackery Spytz9c284492019-11-13 00:13:33 -07002096 self.cursor_blink.set(idleConf.GetOption(
2097 'main', 'EditorWindow', 'cursor-blink', type='bool'))
wohlganger58fc71c2017-09-10 16:19:47 -05002098 self.autocomplete_wait.set(idleConf.GetOption(
2099 'extensions', 'AutoComplete', 'popupwait', type='int'))
2100 self.paren_style.set(idleConf.GetOption(
2101 'extensions', 'ParenMatch', 'style'))
2102 self.flash_delay.set(idleConf.GetOption(
2103 'extensions', 'ParenMatch', 'flash-delay', type='int'))
2104 self.paren_bell.set(idleConf.GetOption(
2105 'extensions', 'ParenMatch', 'bell'))
2106
2107 # Set variables for editor windows.
2108 self.autosave.set(idleConf.GetOption(
2109 'main', 'General', 'autosave', default=0, type='bool'))
2110 self.format_width.set(idleConf.GetOption(
2111 'extensions', 'FormatParagraph', 'max-width', type='int'))
Tal Einat7123ea02019-07-23 15:22:11 +03002112 self.line_numbers_default.set(idleConf.GetOption(
2113 'main', 'EditorWindow', 'line-numbers-default', type='bool'))
wohlganger58fc71c2017-09-10 16:19:47 -05002114 self.context_lines.set(idleConf.GetOption(
Cheryl Sabella29996a12018-06-01 19:23:00 -04002115 'extensions', 'CodeContext', 'maxlines', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002116
Tal Einat604e7b92018-09-25 15:10:14 +03002117 # Set variables for shell windows.
2118 self.auto_squeeze_min_lines.set(idleConf.GetOption(
2119 'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
2120
csabellae8eb17b2017-07-30 18:39:17 -04002121 # Set additional help sources.
2122 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2123 self.helplist.delete(0, 'end')
2124 for help_item in self.user_helplist:
2125 self.helplist.insert(END, help_item[0])
2126 self.set_add_delete_state()
2127
2128 def help_source_selected(self, event):
2129 "Handle event for selecting additional help."
2130 self.set_add_delete_state()
2131
2132 def set_add_delete_state(self):
2133 "Toggle the state for the help list buttons based on list entries."
2134 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002135 self.button_helplist_edit.state(('disabled',))
2136 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002137 else: # Some entries.
2138 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002139 self.button_helplist_edit.state(('!disabled',))
2140 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002141 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002142 self.button_helplist_edit.state(('disabled',))
2143 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002144
2145 def helplist_item_add(self):
2146 """Handle add button for the help list.
2147
2148 Query for name and location of new help sources and add
2149 them to the list.
2150 """
2151 help_source = HelpSource(self, 'New Help Source').result
2152 if help_source:
2153 self.user_helplist.append(help_source)
2154 self.helplist.insert(END, help_source[0])
2155 self.update_help_changes()
2156
2157 def helplist_item_edit(self):
2158 """Handle edit button for the help list.
2159
2160 Query with existing help source information and update
2161 config if the values are changed.
2162 """
2163 item_index = self.helplist.index(ANCHOR)
2164 help_source = self.user_helplist[item_index]
2165 new_help_source = HelpSource(
2166 self, 'Edit Help Source',
2167 menuitem=help_source[0],
2168 filepath=help_source[1],
2169 ).result
2170 if new_help_source and new_help_source != help_source:
2171 self.user_helplist[item_index] = new_help_source
2172 self.helplist.delete(item_index)
2173 self.helplist.insert(item_index, new_help_source[0])
2174 self.update_help_changes()
2175 self.set_add_delete_state() # Selected will be un-selected
2176
2177 def helplist_item_remove(self):
2178 """Handle remove button for the help list.
2179
2180 Delete the help list item from config.
2181 """
2182 item_index = self.helplist.index(ANCHOR)
2183 del(self.user_helplist[item_index])
2184 self.helplist.delete(item_index)
2185 self.update_help_changes()
2186 self.set_add_delete_state()
2187
2188 def update_help_changes(self):
2189 "Clear and rebuild the HelpFiles section in changes"
2190 changes['main']['HelpFiles'] = {}
2191 for num in range(1, len(self.user_helplist) + 1):
2192 changes.add_option(
2193 'main', 'HelpFiles', str(num),
2194 ';'.join(self.user_helplist[num-1][:2]))
2195
2196
csabella45bf7232017-07-26 19:09:58 -04002197class VarTrace:
2198 """Maintain Tk variables trace state."""
2199
2200 def __init__(self):
2201 """Store Tk variables and callbacks.
2202
2203 untraced: List of tuples (var, callback)
2204 that do not have the callback attached
2205 to the Tk var.
2206 traced: List of tuples (var, callback) where
2207 that callback has been attached to the var.
2208 """
2209 self.untraced = []
2210 self.traced = []
2211
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002212 def clear(self):
2213 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002214 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002215 self.untraced.clear()
2216 self.traced.clear()
2217
csabella45bf7232017-07-26 19:09:58 -04002218 def add(self, var, callback):
2219 """Add (var, callback) tuple to untraced list.
2220
2221 Args:
2222 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002223 callback: Either function name to be used as a callback
2224 or a tuple with IdleConf config-type, section, and
2225 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002226
2227 Return:
2228 Tk variable instance.
2229 """
2230 if isinstance(callback, tuple):
2231 callback = self.make_callback(var, callback)
2232 self.untraced.append((var, callback))
2233 return var
2234
2235 @staticmethod
2236 def make_callback(var, config):
2237 "Return default callback function to add values to changes instance."
2238 def default_callback(*params):
2239 "Add config values to changes instance."
2240 changes.add_option(*config, var.get())
2241 return default_callback
2242
2243 def attach(self):
2244 "Attach callback to all vars that are not traced."
2245 while self.untraced:
2246 var, callback = self.untraced.pop()
2247 var.trace_add('write', callback)
2248 self.traced.append((var, callback))
2249
2250 def detach(self):
2251 "Remove callback from traced vars."
2252 while self.traced:
2253 var, callback = self.traced.pop()
2254 var.trace_remove('write', var.trace_info()[0][1])
2255 self.untraced.append((var, callback))
2256
2257
csabella5b591542017-07-28 14:40:59 -04002258tracers = VarTrace()
2259
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002260help_common = '''\
2261When you click either the Apply or Ok buttons, settings in this
2262dialog that are different from IDLE's default are saved in
2263a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002264these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002265machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002266'''
2267help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002268 'Fonts/Tabs':'''
2269Font sample: This shows what a selection of Basic Multilingual Plane
2270unicode characters look like for the current font selection. If the
2271selected font does not define a character, Tk attempts to find another
2272font that does. Substitute glyphs depend on what is available on a
2273particular system and will not necessarily have the same size as the
2274font selected. Line contains 20 characters up to Devanagari, 14 for
2275Tamil, and 10 for East Asia.
2276
2277Hebrew and Arabic letters should display right to left, starting with
2278alef, \u05d0 and \u0627. Arabic digits display left to right. The
2279Devanagari and Tamil lines start with digits. The East Asian lines
2280are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2281Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002282
2283You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002284''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002285 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002286Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002287The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002288be used with older IDLE releases if it is saved as a custom
2289theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002290''',
2291 'Keys': '''
2292Keys:
2293The IDLE Modern Unix key set is new in June 2016. It can only
2294be used with older IDLE releases if it is saved as a custom
2295key set, with a different name.
2296''',
wohlganger58fc71c2017-09-10 16:19:47 -05002297 'General': '''
2298General:
wohlgangerfae2c352017-06-27 21:36:23 -05002299
penguindustin96466302019-05-06 14:57:17 -04002300AutoComplete: Popupwait is milliseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002301cursor movement, before popping up completion box. Key char is '.' after
2302identifier or a '/' (or '\\' on Windows) within a string.
2303
2304FormatParagraph: Max-width is max chars in lines after re-formatting.
2305Use with paragraphs in both strings and comment blocks.
2306
2307ParenMatch: Style indicates what is highlighted when closer is entered:
2308'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2309'expression' (default) - also everything in between. Flash-delay is how
2310long to highlight if cursor is not moved (0 means forever).
Cheryl Sabella29996a12018-06-01 19:23:00 -04002311
2312CodeContext: Maxlines is the maximum number of code context lines to
2313display when Code Context is turned on for an editor window.
Tal Einat604e7b92018-09-25 15:10:14 +03002314
2315Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
2316of output to automatically "squeeze".
wohlgangerfae2c352017-06-27 21:36:23 -05002317'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002318}
2319
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002320
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002321def is_int(s):
2322 "Return 's is blank or represents an int'"
2323 if not s:
2324 return True
2325 try:
2326 int(s)
2327 return True
2328 except ValueError:
2329 return False
2330
2331
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002332class VerticalScrolledFrame(Frame):
2333 """A pure Tkinter vertically scrollable frame.
2334
2335 * Use the 'interior' attribute to place widgets inside the scrollable frame
2336 * Construct and pack/place/grid normally
2337 * This frame only allows vertical scrolling
2338 """
2339 def __init__(self, parent, *args, **kw):
2340 Frame.__init__(self, parent, *args, **kw)
2341
csabella7eb58832017-07-04 21:30:58 -04002342 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002343 vscrollbar = Scrollbar(self, orient=VERTICAL)
2344 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002345 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002346 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002347 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2348 vscrollbar.config(command=canvas.yview)
2349
csabella7eb58832017-07-04 21:30:58 -04002350 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002351 canvas.xview_moveto(0)
2352 canvas.yview_moveto(0)
2353
csabella7eb58832017-07-04 21:30:58 -04002354 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002355 self.interior = interior = Frame(canvas)
2356 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2357
csabella7eb58832017-07-04 21:30:58 -04002358 # Track changes to the canvas and frame width and sync them,
2359 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002360 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002361 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002362 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2363 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002364 interior.bind('<Configure>', _configure_interior)
2365
2366 def _configure_canvas(event):
2367 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002368 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002369 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2370 canvas.bind('<Configure>', _configure_canvas)
2371
2372 return
2373
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002374
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002375if __name__ == '__main__':
Terry Jan Reedy4d921582018-06-19 19:12:52 -04002376 from unittest import main
2377 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
2378
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002379 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002380 run(ConfigDialog)