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