blob: d27b0727d3d4a9759ebce0f20b2b7054c961fe28 [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,
14 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, NORMAL, DISABLED,
15 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
Louie Lubb2bae82017-07-10 06:57:18 +080016 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
Cheryl Sabella7028e592017-08-26 14:26:02 -040017from tkinter.ttk import (Button, Checkbutton, Entry, Frame, Label, LabelFrame,
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 Reedya9421fb2014-10-22 20:15:18 -040028from idlelib.tabbedpages import TabbedPageSet
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040029from idlelib.textview import view_text
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040030from idlelib.autocomplete import AutoComplete
31from idlelib.codecontext import CodeContext
32from idlelib.parenmatch import ParenMatch
33from idlelib.paragraph import FormatParagraph
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.
37reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph)
terryjreedy349abd92017-07-07 16:00:57 -040038
csabella5b591542017-07-28 14:40:59 -040039
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000040class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -040041 """Config dialog for IDLE.
42 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000043
Terry Jan Reedybfebfd82017-09-30 17:37:53 -040044 def __init__(self, parent, title='', *, _htest=False, _utest=False):
csabella7eb58832017-07-04 21:30:58 -040045 """Show the tabbed dialog for user configuration.
46
csabella36329a42017-07-13 23:32:01 -040047 Args:
48 parent - parent of this dialog
49 title - string which is the title of this popup dialog
50 _htest - bool, change box location when running htest
51 _utest - bool, don't wait_window when running unittest
52
53 Note: Focus set on font page fontlist.
54
55 Methods:
56 create_widgets
57 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040058 """
Steven M. Gavad721c482001-07-31 10:46:53 +000059 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040060 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040061 if _htest:
62 parent.instance_dict = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080063 if not _utest:
64 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000065
Steven M. Gavad721c482001-07-31 10:46:53 +000066 self.configure(borderwidth=5)
Terry Jan Reedycd567362014-10-17 01:31:35 -040067 self.title(title or 'IDLE Preferences')
csabellabac7d332017-06-26 17:46:26 -040068 x = parent.winfo_rootx() + 20
69 y = parent.winfo_rooty() + (30 if not _htest else 150)
70 self.geometry(f'+{x}+{y}')
csabella7eb58832017-07-04 21:30:58 -040071 # Each theme element key is its display name.
72 # The first value of the tuple is the sample area tag name.
73 # The second value is the display name list sort index.
csabellabac7d332017-06-26 17:46:26 -040074 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040075 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000076 self.transient(parent)
csabellabac7d332017-06-26 17:46:26 -040077 self.protocol("WM_DELETE_WINDOW", self.cancel)
csabella9397e2a2017-07-30 13:34:25 -040078 self.fontpage.fontlist.focus_set()
csabella7eb58832017-07-04 21:30:58 -040079 # XXX Decide whether to keep or delete these key bindings.
80 # Key bindings for this dialog.
81 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
82 # self.bind('<Alt-a>', self.Apply) #apply changes, save
83 # self.bind('<F1>', self.Help) #context help
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040084 # Attach callbacks after loading config to avoid calling them.
csabella5b591542017-07-28 14:40:59 -040085 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000086
Terry Jan Reedycfa89502014-07-14 23:07:32 -040087 if not _utest:
Louie Lu9b622fb2017-07-14 08:35:48 +080088 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040089 self.wm_deiconify()
90 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000091
csabellabac7d332017-06-26 17:46:26 -040092 def create_widgets(self):
csabella36329a42017-07-13 23:32:01 -040093 """Create and place widgets for tabbed dialog.
94
95 Widgets Bound to self:
csabellae8eb17b2017-07-30 18:39:17 -040096 note: Notebook
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040097 highpage: HighPage
csabellae8eb17b2017-07-30 18:39:17 -040098 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -040099 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -0400100 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400101 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -0400102
103 Methods:
csabella36329a42017-07-13 23:32:01 -0400104 create_action_buttons
105 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400106 activate_config_changes: Tell editors to reload.
107 """
Terry Jan Reedyd6e2f262017-09-19 19:01:45 -0400108 self.note = note = Notebook(self)
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400109 self.highpage = HighPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400110 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400111 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400112 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400113 self.extpage = self.create_page_extensions()
114 note.add(self.fontpage, text='Fonts/Tabs')
115 note.add(self.highpage, text='Highlights')
116 note.add(self.keyspage, text=' Keys ')
117 note.add(self.genpage, text=' General ')
118 note.add(self.extpage, text='Extensions')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400119 note.enable_traversal()
120 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400121 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400122
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400123 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -0400124 """Return frame of action buttons for dialog.
125
126 Methods:
127 ok
128 apply
129 cancel
130 help
131
132 Widget Structure:
133 outer: Frame
134 buttons: Frame
135 (no assignment): Button (ok)
136 (no assignment): Button (apply)
137 (no assignment): Button (cancel)
138 (no assignment): Button (help)
139 (no assignment): Frame
140 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400141 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400142 # Changing the default padding on OSX results in unreadable
csabella7eb58832017-07-04 21:30:58 -0400143 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400144 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000145 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400146 padding_args = {'padding': (6, 3)}
147 outer = Frame(self, padding=2)
148 buttons = Frame(outer, padding=2)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400149 for txt, cmd in (
csabellabac7d332017-06-26 17:46:26 -0400150 ('Ok', self.ok),
151 ('Apply', self.apply),
152 ('Cancel', self.cancel),
153 ('Help', self.help)):
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400154 Button(buttons, text=txt, command=cmd, takefocus=FALSE,
csabellabac7d332017-06-26 17:46:26 -0400155 **padding_args).pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -0400156 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400157 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
158 buttons.pack(side=BOTTOM)
159 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400160
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400161 def ok(self):
162 """Apply config changes, then dismiss dialog.
163
164 Methods:
165 apply
166 destroy: inherited
167 """
168 self.apply()
169 self.destroy()
170
171 def apply(self):
172 """Apply config changes and leave dialog open.
173
174 Methods:
175 deactivate_current_config
176 save_all_changed_extensions
177 activate_config_changes
178 """
179 self.deactivate_current_config()
180 changes.save_all()
181 self.save_all_changed_extensions()
182 self.activate_config_changes()
183
184 def cancel(self):
185 """Dismiss config dialog.
186
187 Methods:
188 destroy: inherited
189 """
190 self.destroy()
191
192 def help(self):
193 """Create textview for config dialog help.
194
195 Attrbutes accessed:
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400196 note
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400197
198 Methods:
199 view_text: Method from textview module.
200 """
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400201 page = self.note.tab(self.note.select(), option='text').strip()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400202 view_text(self, title='Help for IDLE preferences',
203 text=help_common+help_pages.get(page, ''))
204
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400205 def deactivate_current_config(self):
206 """Remove current key bindings.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400207 Iterate over window instances defined in parent and remove
208 the keybindings.
209 """
210 # Before a config is saved, some cleanup of current
211 # config must be done - remove the previous keybindings.
212 win_instances = self.parent.instance_dict.keys()
213 for instance in win_instances:
214 instance.RemoveKeybindings()
215
216 def activate_config_changes(self):
217 """Apply configuration changes to current windows.
218
219 Dynamically update the current parent window instances
220 with some of the configuration changes.
221 """
222 win_instances = self.parent.instance_dict.keys()
223 for instance in win_instances:
224 instance.ResetColorizer()
225 instance.ResetFont()
226 instance.set_notabs_indentwidth()
227 instance.ApplyKeybindings()
228 instance.reset_help_menu_entries()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -0400229 for klass in reloadables:
230 klass.reload()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400231
csabellabac7d332017-06-26 17:46:26 -0400232 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400233 """Part of the config dialog used for configuring IDLE extensions.
234
235 This code is generic - it works for any and all IDLE extensions.
236
237 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400238 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400239 GUI interface to change the configuration values, and saves the
240 changes using idleConf.
241
242 Not all changes take effect immediately - some may require restarting IDLE.
243 This depends on each extension's implementation.
244
245 All values are treated as text, and it is up to the user to supply
246 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300247 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -0400248
249 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300250 load_extensions:
csabella36329a42017-07-13 23:32:01 -0400251 extension_selected: Handle selection from list.
252 create_extension_frame: Hold widgets for one extension.
253 set_extension_value: Set in userCfg['extensions'].
254 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400255 """
256 parent = self.parent
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400257 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400258 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
259 self.ext_userCfg = idleConf.userCfg['extensions']
260 self.is_int = self.register(is_int)
261 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -0400262 # Create widgets - a listbox shows all available extensions, with the
263 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400264 self.extension_names = StringVar(self)
265 frame.rowconfigure(0, weight=1)
266 frame.columnconfigure(2, weight=1)
267 self.extension_list = Listbox(frame, listvariable=self.extension_names,
268 selectmode='browse')
269 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
270 scroll = Scrollbar(frame, command=self.extension_list.yview)
271 self.extension_list.yscrollcommand=scroll.set
272 self.details_frame = LabelFrame(frame, width=250, height=250)
273 self.extension_list.grid(column=0, row=0, sticky='nws')
274 scroll.grid(column=1, row=0, sticky='ns')
275 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
Cheryl Sabella7028e592017-08-26 14:26:02 -0400276 frame.configure(padding=10)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400277 self.config_frame = {}
278 self.current_extension = None
279
280 self.outerframe = self # TEMPORARY
281 self.tabbed_page_set = self.extension_list # TEMPORARY
282
csabella7eb58832017-07-04 21:30:58 -0400283 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400284 ext_names = ''
285 for ext_name in sorted(self.extensions):
286 self.create_extension_frame(ext_name)
287 ext_names = ext_names + '{' + ext_name + '} '
288 self.extension_names.set(ext_names)
289 self.extension_list.selection_set(0)
290 self.extension_selected(None)
291
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400292 return frame
293
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400294 def load_extensions(self):
295 "Fill self.extensions with data from the default and user configs."
296 self.extensions = {}
297 for ext_name in idleConf.GetExtensions(active_only=False):
wohlganger58fc71c2017-09-10 16:19:47 -0500298 # Former built-in extensions are already filtered out.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400299 self.extensions[ext_name] = []
300
301 for ext_name in self.extensions:
302 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
303
csabella7eb58832017-07-04 21:30:58 -0400304 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400305 enables = [opt_name for opt_name in opt_list
306 if opt_name.startswith('enable')]
307 for opt_name in enables:
308 opt_list.remove(opt_name)
309 opt_list = enables + opt_list
310
311 for opt_name in opt_list:
312 def_str = self.ext_defaultCfg.Get(
313 ext_name, opt_name, raw=True)
314 try:
315 def_obj = {'True':True, 'False':False}[def_str]
316 opt_type = 'bool'
317 except KeyError:
318 try:
319 def_obj = int(def_str)
320 opt_type = 'int'
321 except ValueError:
322 def_obj = def_str
323 opt_type = None
324 try:
325 value = self.ext_userCfg.Get(
326 ext_name, opt_name, type=opt_type, raw=True,
327 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -0400328 except ValueError: # Need this until .Get fixed.
329 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400330 var = StringVar(self)
331 var.set(str(value))
332
333 self.extensions[ext_name].append({'name': opt_name,
334 'type': opt_type,
335 'default': def_str,
336 'value': value,
337 'var': var,
338 })
339
340 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -0400341 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400342 newsel = self.extension_list.curselection()
343 if newsel:
344 newsel = self.extension_list.get(newsel)
345 if newsel is None or newsel != self.current_extension:
346 if self.current_extension:
347 self.details_frame.config(text='')
348 self.config_frame[self.current_extension].grid_forget()
349 self.current_extension = None
350 if newsel:
351 self.details_frame.config(text=newsel)
352 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
353 self.current_extension = newsel
354
355 def create_extension_frame(self, ext_name):
356 """Create a frame holding the widgets to configure one extension"""
357 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
358 self.config_frame[ext_name] = f
359 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -0400360 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400361 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -0400362 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400363 label = Label(entry_area, text=opt['name'])
364 label.grid(row=row, column=0, sticky=NW)
365 var = opt['var']
366 if opt['type'] == 'bool':
Cheryl Sabella7028e592017-08-26 14:26:02 -0400367 Checkbutton(entry_area, variable=var,
368 onvalue='True', offvalue='False', width=8
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400369 ).grid(row=row, column=1, sticky=W, padx=7)
370 elif opt['type'] == 'int':
371 Entry(entry_area, textvariable=var, validate='key',
372 validatecommand=(self.is_int, '%P')
373 ).grid(row=row, column=1, sticky=NSEW, padx=7)
374
375 else:
376 Entry(entry_area, textvariable=var
377 ).grid(row=row, column=1, sticky=NSEW, padx=7)
378 return
379
380 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400381 """Return True if the configuration was added or changed.
382
383 If the value is the same as the default, then remove it
384 from user config file.
385 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400386 name = opt['name']
387 default = opt['default']
388 value = opt['var'].get().strip() or default
389 opt['var'].set(value)
390 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400391 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400392 if (value == default):
393 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400394 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400395 return self.ext_userCfg.SetOption(section, name, value)
396
397 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400398 """Save configuration changes to the user config file.
399
400 Attributes accessed:
401 extensions
402
403 Methods:
404 set_extension_value
405 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400406 has_changes = False
407 for ext_name in self.extensions:
408 options = self.extensions[ext_name]
409 for opt in options:
410 if self.set_extension_value(ext_name, opt):
411 has_changes = True
412 if has_changes:
413 self.ext_userCfg.Save()
414
415
csabella6f446be2017-08-01 00:24:07 -0400416# class TabPage(Frame): # A template for Page classes.
417# def __init__(self, master):
418# super().__init__(master)
419# self.create_page_tab()
420# self.load_tab_cfg()
421# def create_page_tab(self):
422# # Define tk vars and register var and callback with tracers.
423# # Create subframes and widgets.
424# # Pack widgets.
425# def load_tab_cfg(self):
426# # Initialize widgets with data from idleConf.
427# def var_changed_var_name():
428# # For each tk var that needs other than default callback.
429# def other_methods():
430# # Define tab-specific behavior.
431
432
csabella9397e2a2017-07-30 13:34:25 -0400433class FontPage(Frame):
434
csabella6f446be2017-08-01 00:24:07 -0400435 def __init__(self, master, highpage):
436 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400437 self.highlight_sample = highpage.highlight_sample
438 self.create_page_font_tab()
439 self.load_font_cfg()
440 self.load_tab_cfg()
441
442 def create_page_font_tab(self):
443 """Return frame of widgets for Font/Tabs tab.
444
445 Fonts: Enable users to provisionally change font face, size, or
446 boldness and to see the consequence of proposed choices. Each
447 action set 3 options in changes structuree and changes the
448 corresponding aspect of the font sample on this page and
449 highlight sample on highlight page.
450
451 Function load_font_cfg initializes font vars and widgets from
452 idleConf entries and tk.
453
454 Fontlist: mouse button 1 click or up or down key invoke
455 on_fontlist_select(), which sets var font_name.
456
457 Sizelist: clicking the menubutton opens the dropdown menu. A
458 mouse button 1 click or return key sets var font_size.
459
460 Bold_toggle: clicking the box toggles var font_bold.
461
462 Changing any of the font vars invokes var_changed_font, which
463 adds all 3 font options to changes and calls set_samples.
464 Set_samples applies a new font constructed from the font vars to
465 font_sample and to highlight_sample on the hightlight page.
466
467 Tabs: Enable users to change spaces entered for indent tabs.
468 Changing indent_scale value with the mouse sets Var space_num,
469 which invokes the default callback to add an entry to
470 changes. Load_tab_cfg initializes space_num to default.
471
Cheryl Sabella2f896462017-08-14 21:21:43 -0400472 Widgets for FontPage(Frame): (*) widgets bound to self
473 frame_font: LabelFrame
474 frame_font_name: Frame
475 font_name_title: Label
476 (*)fontlist: ListBox - font_name
477 scroll_font: Scrollbar
478 frame_font_param: Frame
479 font_size_title: Label
480 (*)sizelist: DynOptionMenu - font_size
481 (*)bold_toggle: Checkbutton - font_bold
482 frame_font_sample: Frame
483 (*)font_sample: Label
484 frame_indent: LabelFrame
485 indent_title: Label
486 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400487 """
csabella6f446be2017-08-01 00:24:07 -0400488 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
489 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
490 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400491 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
492
493 # Create widgets:
494 # body and body section frames.
csabella9397e2a2017-07-30 13:34:25 -0400495 frame_font = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400496 self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
csabella9397e2a2017-07-30 13:34:25 -0400497 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400498 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400499 # frame_font.
500 frame_font_name = Frame(frame_font)
501 frame_font_param = Frame(frame_font)
502 font_name_title = Label(
503 frame_font_name, justify=LEFT, text='Font Face :')
504 self.fontlist = Listbox(frame_font_name, height=5,
505 takefocus=True, exportselection=FALSE)
506 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
507 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
508 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
509 scroll_font = Scrollbar(frame_font_name)
510 scroll_font.config(command=self.fontlist.yview)
511 self.fontlist.config(yscrollcommand=scroll_font.set)
512 font_size_title = Label(frame_font_param, text='Size :')
513 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
514 self.bold_toggle = Checkbutton(
515 frame_font_param, variable=self.font_bold,
516 onvalue=1, offvalue=0, text='Bold')
517 frame_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1)
csabella6f446be2017-08-01 00:24:07 -0400518 temp_font = tkFont.Font(self, ('courier', 10, 'normal'))
csabella9397e2a2017-07-30 13:34:25 -0400519 self.font_sample = Label(
520 frame_font_sample, justify=LEFT, font=temp_font,
521 text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]')
522 # frame_indent.
523 indent_title = Label(
524 frame_indent, justify=LEFT,
525 text='Python Standard: 4 Spaces!')
526 self.indent_scale = Scale(
527 frame_indent, variable=self.space_num,
528 orient='horizontal', tickinterval=2, from_=2, to=16)
529
530 # Pack widgets:
531 # body.
532 frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
533 frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y)
534 # frame_font.
535 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
536 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
537 font_name_title.pack(side=TOP, anchor=W)
538 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
539 scroll_font.pack(side=LEFT, fill=Y)
540 font_size_title.pack(side=LEFT, anchor=W)
541 self.sizelist.pack(side=LEFT, anchor=W)
542 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
543 frame_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
544 self.font_sample.pack(expand=TRUE, fill=BOTH)
545 # frame_indent.
546 frame_indent.pack(side=TOP, fill=X)
547 indent_title.pack(side=TOP, anchor=W, padx=5)
548 self.indent_scale.pack(side=TOP, padx=5, fill=X)
549
csabella9397e2a2017-07-30 13:34:25 -0400550 def load_font_cfg(self):
551 """Load current configuration settings for the font options.
552
553 Retrieve current font with idleConf.GetFont and font families
554 from tk. Setup fontlist and set font_name. Setup sizelist,
555 which sets font_size. Set font_bold. Call set_samples.
556 """
557 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
558 font_name = configured_font[0].lower()
559 font_size = configured_font[1]
560 font_bold = configured_font[2]=='bold'
561
562 # Set editor font selection list and font_name.
563 fonts = list(tkFont.families(self))
564 fonts.sort()
565 for font in fonts:
566 self.fontlist.insert(END, font)
567 self.font_name.set(font_name)
568 lc_fonts = [s.lower() for s in fonts]
569 try:
570 current_font_index = lc_fonts.index(font_name)
571 self.fontlist.see(current_font_index)
572 self.fontlist.select_set(current_font_index)
573 self.fontlist.select_anchor(current_font_index)
574 self.fontlist.activate(current_font_index)
575 except ValueError:
576 pass
577 # Set font size dropdown.
578 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
579 '16', '18', '20', '22', '25', '29', '34', '40'),
580 font_size)
581 # Set font weight.
582 self.font_bold.set(font_bold)
583 self.set_samples()
584
585 def var_changed_font(self, *params):
586 """Store changes to font attributes.
587
588 When one font attribute changes, save them all, as they are
589 not independent from each other. In particular, when we are
590 overriding the default font, we need to write out everything.
591 """
592 value = self.font_name.get()
593 changes.add_option('main', 'EditorWindow', 'font', value)
594 value = self.font_size.get()
595 changes.add_option('main', 'EditorWindow', 'font-size', value)
596 value = self.font_bold.get()
597 changes.add_option('main', 'EditorWindow', 'font-bold', value)
598 self.set_samples()
599
600 def on_fontlist_select(self, event):
601 """Handle selecting a font from the list.
602
603 Event can result from either mouse click or Up or Down key.
604 Set font_name and example displays to selection.
605 """
606 font = self.fontlist.get(
607 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
608 self.font_name.set(font.lower())
609
610 def set_samples(self, event=None):
611 """Update update both screen samples with the font settings.
612
613 Called on font initialization and change events.
614 Accesses font_name, font_size, and font_bold Variables.
615 Updates font_sample and hightlight page highlight_sample.
616 """
617 font_name = self.font_name.get()
618 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
619 new_font = (font_name, self.font_size.get(), font_weight)
620 self.font_sample['font'] = new_font
621 self.highlight_sample['font'] = new_font
622
623 def load_tab_cfg(self):
624 """Load current configuration settings for the tab options.
625
626 Attributes updated:
627 space_num: Set to value from idleConf.
628 """
629 # Set indent sizes.
630 space_num = idleConf.GetOption(
631 'main', 'Indent', 'num-spaces', default=4, type='int')
632 self.space_num.set(space_num)
633
634 def var_changed_space_num(self, *params):
635 "Store change to indentation size."
636 value = self.space_num.get()
637 changes.add_option('main', 'Indent', 'num-spaces', value)
638
639
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400640class HighPage(Frame):
641
642 def __init__(self, master):
643 super().__init__(master)
644 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400645 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400646 self.create_page_highlight()
647 self.load_theme_cfg()
648
649 def create_page_highlight(self):
650 """Return frame of widgets for Highlighting tab.
651
652 Enable users to provisionally change foreground and background
653 colors applied to textual tags. Color mappings are stored in
654 complete listings called themes. Built-in themes in
655 idlelib/config-highlight.def are fixed as far as the dialog is
656 concerned. Any theme can be used as the base for a new custom
657 theme, stored in .idlerc/config-highlight.cfg.
658
659 Function load_theme_cfg() initializes tk variables and theme
660 lists and calls paint_theme_sample() and set_highlight_target()
661 for the current theme. Radiobuttons builtin_theme_on and
662 custom_theme_on toggle var theme_source, which controls if the
663 current set of colors are from a builtin or custom theme.
664 DynOptionMenus builtinlist and customlist contain lists of the
665 builtin and custom themes, respectively, and the current item
666 from each list is stored in vars builtin_name and custom_name.
667
668 Function paint_theme_sample() applies the colors from the theme
669 to the tags in text widget highlight_sample and then invokes
670 set_color_sample(). Function set_highlight_target() sets the state
671 of the radiobuttons fg_on and bg_on based on the tag and it also
672 invokes set_color_sample().
673
674 Function set_color_sample() sets the background color for the frame
675 holding the color selector. This provides a larger visual of the
676 color for the current tag and plane (foreground/background).
677
678 Note: set_color_sample() is called from many places and is often
679 called more than once when a change is made. It is invoked when
680 foreground or background is selected (radiobuttons), from
681 paint_theme_sample() (theme is changed or load_cfg is called), and
682 from set_highlight_target() (target tag is changed or load_cfg called).
683
684 Button delete_custom invokes delete_custom() to delete
685 a custom theme from idleConf.userCfg['highlight'] and changes.
686 Button save_custom invokes save_as_new_theme() which calls
687 get_new_theme_name() and create_new() to save a custom theme
688 and its colors to idleConf.userCfg['highlight'].
689
690 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
691 if the current selected color for a tag is for the foreground or
692 background.
693
694 DynOptionMenu targetlist contains a readable description of the
695 tags applied to Python source within IDLE. Selecting one of the
696 tags from this list populates highlight_target, which has a callback
697 function set_highlight_target().
698
699 Text widget highlight_sample displays a block of text (which is
700 mock Python code) in which is embedded the defined tags and reflects
701 the color attributes of the current theme and changes for those tags.
702 Mouse button 1 allows for selection of a tag and updates
703 highlight_target with that tag value.
704
705 Note: The font in highlight_sample is set through the config in
706 the fonts tab.
707
708 In other words, a tag can be selected either from targetlist or
709 by clicking on the sample text within highlight_sample. The
710 plane (foreground/background) is selected via the radiobutton.
711 Together, these two (tag and plane) control what color is
712 shown in set_color_sample() for the current theme. Button set_color
713 invokes get_color() which displays a ColorChooser to change the
714 color for the selected tag/plane. If a new color is picked,
715 it will be saved to changes and the highlight_sample and
716 frame background will be updated.
717
718 Tk Variables:
719 color: Color of selected target.
720 builtin_name: Menu variable for built-in theme.
721 custom_name: Menu variable for custom theme.
722 fg_bg_toggle: Toggle for foreground/background color.
723 Note: this has no callback.
724 theme_source: Selector for built-in or custom theme.
725 highlight_target: Menu variable for the highlight tag target.
726
727 Instance Data Attributes:
728 theme_elements: Dictionary of tags for text highlighting.
729 The key is the display name and the value is a tuple of
730 (tag name, display sort order).
731
732 Methods [attachment]:
733 load_theme_cfg: Load current highlight colors.
734 get_color: Invoke colorchooser [button_set_color].
735 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
736 set_highlight_target: set fg_bg_toggle, set_color_sample().
737 set_color_sample: Set frame background to target.
738 on_new_color_set: Set new color and add option.
739 paint_theme_sample: Recolor sample.
740 get_new_theme_name: Get from popup.
741 create_new: Combine theme with changes and save.
742 save_as_new_theme: Save [button_save_custom].
743 set_theme_type: Command for [theme_source].
744 delete_custom: Activate default [button_delete_custom].
745 save_new: Save to userCfg['theme'] (is function).
746
747 Widgets of highlights page frame: (*) widgets bound to self
748 frame_custom: LabelFrame
749 (*)highlight_sample: Text
750 (*)frame_color_set: Frame
751 (*)button_set_color: Button
752 (*)targetlist: DynOptionMenu - highlight_target
753 frame_fg_bg_toggle: Frame
754 (*)fg_on: Radiobutton - fg_bg_toggle
755 (*)bg_on: Radiobutton - fg_bg_toggle
756 (*)button_save_custom: Button
757 frame_theme: LabelFrame
758 theme_type_title: Label
759 (*)builtin_theme_on: Radiobutton - theme_source
760 (*)custom_theme_on: Radiobutton - theme_source
761 (*)builtinlist: DynOptionMenu - builtin_name
762 (*)customlist: DynOptionMenu - custom_name
763 (*)button_delete_custom: Button
764 (*)theme_message: Label
765 """
766 self.theme_elements = {
767 'Normal Text': ('normal', '00'),
768 'Python Keywords': ('keyword', '01'),
769 'Python Definitions': ('definition', '02'),
770 'Python Builtins': ('builtin', '03'),
771 'Python Comments': ('comment', '04'),
772 'Python Strings': ('string', '05'),
773 'Selected Text': ('hilite', '06'),
774 'Found Text': ('hit', '07'),
775 'Cursor': ('cursor', '08'),
776 'Editor Breakpoint': ('break', '09'),
777 'Shell Normal Text': ('console', '10'),
778 'Shell Error Text': ('error', '11'),
779 'Shell Stdout Text': ('stdout', '12'),
780 'Shell Stderr Text': ('stderr', '13'),
781 }
782 self.builtin_name = tracers.add(
783 StringVar(self), self.var_changed_builtin_name)
784 self.custom_name = tracers.add(
785 StringVar(self), self.var_changed_custom_name)
786 self.fg_bg_toggle = BooleanVar(self)
787 self.color = tracers.add(
788 StringVar(self), self.var_changed_color)
789 self.theme_source = tracers.add(
790 BooleanVar(self), self.var_changed_theme_source)
791 self.highlight_target = tracers.add(
792 StringVar(self), self.var_changed_highlight_target)
793
794 # Create widgets:
795 # body frame and section frames.
796 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
797 text=' Custom Highlighting ')
798 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
799 text=' Highlighting Theme ')
800 # frame_custom.
801 text = self.highlight_sample = Text(
802 frame_custom, relief=SOLID, borderwidth=1,
803 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
804 takefocus=FALSE, highlightthickness=0, wrap=NONE)
805 text.bind('<Double-Button-1>', lambda e: 'break')
806 text.bind('<B1-Motion>', lambda e: 'break')
wohlganger58fc71c2017-09-10 16:19:47 -0500807 text_and_tags=(
808 ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400809 ('#you can click here', 'comment'), ('\n', 'normal'),
810 ('#to choose items', 'comment'), ('\n', 'normal'),
811 ('def', 'keyword'), (' ', 'normal'),
812 ('func', 'definition'), ('(param):\n ', 'normal'),
813 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
814 ("'string'", 'string'), ('\n var1 = ', 'normal'),
815 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
816 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
817 ('list', 'builtin'), ('(', 'normal'),
818 ('None', 'keyword'), (')\n', 'normal'),
819 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
820 (' error ', 'error'), (' ', 'normal'),
821 ('cursor |', 'cursor'), ('\n ', 'normal'),
822 ('shell', 'console'), (' ', 'normal'),
823 ('stdout', 'stdout'), (' ', 'normal'),
824 ('stderr', 'stderr'), ('\n\n', 'normal'))
825 for texttag in text_and_tags:
826 text.insert(END, texttag[0], texttag[1])
827 for element in self.theme_elements:
828 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400829 # event.widget.winfo_top_level().highlight_target.set(elem)
830 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400831 text.tag_bind(
832 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400833 text['state'] = 'disabled'
834 self.style.configure('frame_color_set.TFrame', borderwidth=1,
835 relief='solid')
836 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400837 frame_fg_bg_toggle = Frame(frame_custom)
838 self.button_set_color = Button(
839 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400840 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400841 self.targetlist = DynOptionMenu(
842 self.frame_color_set, self.highlight_target, None,
843 highlightthickness=0) #, command=self.set_highlight_targetBinding
844 self.fg_on = Radiobutton(
845 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
846 text='Foreground', command=self.set_color_sample_binding)
847 self.bg_on = Radiobutton(
848 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
849 text='Background', command=self.set_color_sample_binding)
850 self.fg_bg_toggle.set(1)
851 self.button_save_custom = Button(
852 frame_custom, text='Save as New Custom Theme',
853 command=self.save_as_new_theme)
854 # frame_theme.
855 theme_type_title = Label(frame_theme, text='Select : ')
856 self.builtin_theme_on = Radiobutton(
857 frame_theme, variable=self.theme_source, value=1,
858 command=self.set_theme_type, text='a Built-in Theme')
859 self.custom_theme_on = Radiobutton(
860 frame_theme, variable=self.theme_source, value=0,
861 command=self.set_theme_type, text='a Custom Theme')
862 self.builtinlist = DynOptionMenu(
863 frame_theme, self.builtin_name, None, command=None)
864 self.customlist = DynOptionMenu(
865 frame_theme, self.custom_name, None, command=None)
866 self.button_delete_custom = Button(
867 frame_theme, text='Delete Custom Theme',
868 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400869 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400870 # Pack widgets:
871 # body.
872 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500873 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400874 # frame_custom.
875 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
876 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
877 self.highlight_sample.pack(
878 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
879 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
880 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
881 self.fg_on.pack(side=LEFT, anchor=E)
882 self.bg_on.pack(side=RIGHT, anchor=W)
883 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
884 # frame_theme.
885 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
886 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
887 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
888 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
889 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
890 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
891 self.theme_message.pack(side=TOP, fill=X, pady=5)
892
893 def load_theme_cfg(self):
894 """Load current configuration settings for the theme options.
895
896 Based on the theme_source toggle, the theme is set as
897 either builtin or custom and the initial widget values
898 reflect the current settings from idleConf.
899
900 Attributes updated:
901 theme_source: Set from idleConf.
902 builtinlist: List of default themes from idleConf.
903 customlist: List of custom themes from idleConf.
904 custom_theme_on: Disabled if there are no custom themes.
905 custom_theme: Message with additional information.
906 targetlist: Create menu from self.theme_elements.
907
908 Methods:
909 set_theme_type
910 paint_theme_sample
911 set_highlight_target
912 """
913 # Set current theme type radiobutton.
914 self.theme_source.set(idleConf.GetOption(
915 'main', 'Theme', 'default', type='bool', default=1))
916 # Set current theme.
917 current_option = idleConf.CurrentTheme()
918 # Load available theme option menus.
919 if self.theme_source.get(): # Default theme selected.
920 item_list = idleConf.GetSectionList('default', 'highlight')
921 item_list.sort()
922 self.builtinlist.SetMenu(item_list, current_option)
923 item_list = idleConf.GetSectionList('user', 'highlight')
924 item_list.sort()
925 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400926 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400927 self.custom_name.set('- no custom themes -')
928 else:
929 self.customlist.SetMenu(item_list, item_list[0])
930 else: # User theme selected.
931 item_list = idleConf.GetSectionList('user', 'highlight')
932 item_list.sort()
933 self.customlist.SetMenu(item_list, current_option)
934 item_list = idleConf.GetSectionList('default', 'highlight')
935 item_list.sort()
936 self.builtinlist.SetMenu(item_list, item_list[0])
937 self.set_theme_type()
938 # Load theme element option menu.
939 theme_names = list(self.theme_elements.keys())
940 theme_names.sort(key=lambda x: self.theme_elements[x][1])
941 self.targetlist.SetMenu(theme_names, theme_names[0])
942 self.paint_theme_sample()
943 self.set_highlight_target()
944
945 def var_changed_builtin_name(self, *params):
946 """Process new builtin theme selection.
947
948 Add the changed theme's name to the changed_items and recreate
949 the sample with the values from the selected theme.
950 """
951 old_themes = ('IDLE Classic', 'IDLE New')
952 value = self.builtin_name.get()
953 if value not in old_themes:
954 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
955 changes.add_option('main', 'Theme', 'name', old_themes[0])
956 changes.add_option('main', 'Theme', 'name2', value)
957 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400958 else:
959 changes.add_option('main', 'Theme', 'name', value)
960 changes.add_option('main', 'Theme', 'name2', '')
961 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400962 self.paint_theme_sample()
963
964 def var_changed_custom_name(self, *params):
965 """Process new custom theme selection.
966
967 If a new custom theme is selected, add the name to the
968 changed_items and apply the theme to the sample.
969 """
970 value = self.custom_name.get()
971 if value != '- no custom themes -':
972 changes.add_option('main', 'Theme', 'name', value)
973 self.paint_theme_sample()
974
975 def var_changed_theme_source(self, *params):
976 """Process toggle between builtin and custom theme.
977
978 Update the default toggle value and apply the newly
979 selected theme type.
980 """
981 value = self.theme_source.get()
982 changes.add_option('main', 'Theme', 'default', value)
983 if value:
984 self.var_changed_builtin_name()
985 else:
986 self.var_changed_custom_name()
987
988 def var_changed_color(self, *params):
989 "Process change to color choice."
990 self.on_new_color_set()
991
992 def var_changed_highlight_target(self, *params):
993 "Process selection of new target tag for highlighting."
994 self.set_highlight_target()
995
996 def set_theme_type(self):
997 """Set available screen options based on builtin or custom theme.
998
999 Attributes accessed:
1000 theme_source
1001
1002 Attributes updated:
1003 builtinlist
1004 customlist
1005 button_delete_custom
1006 custom_theme_on
1007
1008 Called from:
1009 handler for builtin_theme_on and custom_theme_on
1010 delete_custom
1011 create_new
1012 load_theme_cfg
1013 """
1014 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001015 self.builtinlist['state'] = 'normal'
1016 self.customlist['state'] = 'disabled'
1017 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001018 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001019 self.builtinlist['state'] = 'disabled'
1020 self.custom_theme_on.state(('!disabled',))
1021 self.customlist['state'] = 'normal'
1022 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001023
1024 def get_color(self):
1025 """Handle button to select a new color for the target tag.
1026
1027 If a new color is selected while using a builtin theme, a
1028 name must be supplied to create a custom theme.
1029
1030 Attributes accessed:
1031 highlight_target
1032 frame_color_set
1033 theme_source
1034
1035 Attributes updated:
1036 color
1037
1038 Methods:
1039 get_new_theme_name
1040 create_new
1041 """
1042 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001043 prev_color = self.style.lookup(self.frame_color_set['style'],
1044 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001045 rgbTuplet, color_string = tkColorChooser.askcolor(
1046 parent=self, title='Pick new color for : '+target,
1047 initialcolor=prev_color)
1048 if color_string and (color_string != prev_color):
1049 # User didn't cancel and they chose a new color.
1050 if self.theme_source.get(): # Current theme is a built-in.
1051 message = ('Your changes will be saved as a new Custom Theme. '
1052 'Enter a name for your new Custom Theme below.')
1053 new_theme = self.get_new_theme_name(message)
1054 if not new_theme: # User cancelled custom theme creation.
1055 return
1056 else: # Create new custom theme based on previously active theme.
1057 self.create_new(new_theme)
1058 self.color.set(color_string)
1059 else: # Current theme is user defined.
1060 self.color.set(color_string)
1061
1062 def on_new_color_set(self):
1063 "Display sample of new color selection on the dialog."
1064 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001065 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001066 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1067 sample_element = self.theme_elements[self.highlight_target.get()][0]
1068 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1069 theme = self.custom_name.get()
1070 theme_element = sample_element + '-' + plane
1071 changes.add_option('highlight', theme, theme_element, new_color)
1072
1073 def get_new_theme_name(self, message):
1074 "Return name of new theme from query popup."
1075 used_names = (idleConf.GetSectionList('user', 'highlight') +
1076 idleConf.GetSectionList('default', 'highlight'))
1077 new_theme = SectionName(
1078 self, 'New Custom Theme', message, used_names).result
1079 return new_theme
1080
1081 def save_as_new_theme(self):
1082 """Prompt for new theme name and create the theme.
1083
1084 Methods:
1085 get_new_theme_name
1086 create_new
1087 """
1088 new_theme_name = self.get_new_theme_name('New Theme Name:')
1089 if new_theme_name:
1090 self.create_new(new_theme_name)
1091
1092 def create_new(self, new_theme_name):
1093 """Create a new custom theme with the given name.
1094
1095 Create the new theme based on the previously active theme
1096 with the current changes applied. Once it is saved, then
1097 activate the new theme.
1098
1099 Attributes accessed:
1100 builtin_name
1101 custom_name
1102
1103 Attributes updated:
1104 customlist
1105 theme_source
1106
1107 Method:
1108 save_new
1109 set_theme_type
1110 """
1111 if self.theme_source.get():
1112 theme_type = 'default'
1113 theme_name = self.builtin_name.get()
1114 else:
1115 theme_type = 'user'
1116 theme_name = self.custom_name.get()
1117 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1118 # Apply any of the old theme's unsaved changes to the new theme.
1119 if theme_name in changes['highlight']:
1120 theme_changes = changes['highlight'][theme_name]
1121 for element in theme_changes:
1122 new_theme[element] = theme_changes[element]
1123 # Save the new theme.
1124 self.save_new(new_theme_name, new_theme)
1125 # Change GUI over to the new theme.
1126 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1127 custom_theme_list.sort()
1128 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1129 self.theme_source.set(0)
1130 self.set_theme_type()
1131
1132 def set_highlight_target(self):
1133 """Set fg/bg toggle and color based on highlight tag target.
1134
1135 Instance variables accessed:
1136 highlight_target
1137
1138 Attributes updated:
1139 fg_on
1140 bg_on
1141 fg_bg_toggle
1142
1143 Methods:
1144 set_color_sample
1145
1146 Called from:
1147 var_changed_highlight_target
1148 load_theme_cfg
1149 """
1150 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001151 self.fg_on.state(('disabled',))
1152 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001153 self.fg_bg_toggle.set(1)
1154 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001155 self.fg_on.state(('!disabled',))
1156 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001157 self.fg_bg_toggle.set(1)
1158 self.set_color_sample()
1159
1160 def set_color_sample_binding(self, *args):
1161 """Change color sample based on foreground/background toggle.
1162
1163 Methods:
1164 set_color_sample
1165 """
1166 self.set_color_sample()
1167
1168 def set_color_sample(self):
1169 """Set the color of the frame background to reflect the selected target.
1170
1171 Instance variables accessed:
1172 theme_elements
1173 highlight_target
1174 fg_bg_toggle
1175 highlight_sample
1176
1177 Attributes updated:
1178 frame_color_set
1179 """
1180 # Set the color sample area.
1181 tag = self.theme_elements[self.highlight_target.get()][0]
1182 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1183 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001184 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001185
1186 def paint_theme_sample(self):
1187 """Apply the theme colors to each element tag in the sample text.
1188
1189 Instance attributes accessed:
1190 theme_elements
1191 theme_source
1192 builtin_name
1193 custom_name
1194
1195 Attributes updated:
1196 highlight_sample: Set the tag elements to the theme.
1197
1198 Methods:
1199 set_color_sample
1200
1201 Called from:
1202 var_changed_builtin_name
1203 var_changed_custom_name
1204 load_theme_cfg
1205 """
1206 if self.theme_source.get(): # Default theme
1207 theme = self.builtin_name.get()
1208 else: # User theme
1209 theme = self.custom_name.get()
1210 for element_title in self.theme_elements:
1211 element = self.theme_elements[element_title][0]
1212 colors = idleConf.GetHighlight(theme, element)
1213 if element == 'cursor': # Cursor sample needs special painting.
1214 colors['background'] = idleConf.GetHighlight(
1215 theme, 'normal', fgBg='bg')
1216 # Handle any unsaved changes to this theme.
1217 if theme in changes['highlight']:
1218 theme_dict = changes['highlight'][theme]
1219 if element + '-foreground' in theme_dict:
1220 colors['foreground'] = theme_dict[element + '-foreground']
1221 if element + '-background' in theme_dict:
1222 colors['background'] = theme_dict[element + '-background']
1223 self.highlight_sample.tag_config(element, **colors)
1224 self.set_color_sample()
1225
1226 def save_new(self, theme_name, theme):
1227 """Save a newly created theme to idleConf.
1228
1229 theme_name - string, the name of the new theme
1230 theme - dictionary containing the new theme
1231 """
1232 if not idleConf.userCfg['highlight'].has_section(theme_name):
1233 idleConf.userCfg['highlight'].add_section(theme_name)
1234 for element in theme:
1235 value = theme[element]
1236 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1237
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001238 def askyesno(self, *args, **kwargs):
1239 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001240 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001241
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001242 def delete_custom(self):
1243 """Handle event to delete custom theme.
1244
1245 The current theme is deactivated and the default theme is
1246 activated. The custom theme is permanently removed from
1247 the config file.
1248
1249 Attributes accessed:
1250 custom_name
1251
1252 Attributes updated:
1253 custom_theme_on
1254 customlist
1255 theme_source
1256 builtin_name
1257
1258 Methods:
1259 deactivate_current_config
1260 save_all_changed_extensions
1261 activate_config_changes
1262 set_theme_type
1263 """
1264 theme_name = self.custom_name.get()
1265 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001266 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001267 'Delete Theme', delmsg % theme_name, parent=self):
1268 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001269 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001270 # Remove theme from changes, config, and file.
1271 changes.delete_section('highlight', theme_name)
1272 # Reload user theme list.
1273 item_list = idleConf.GetSectionList('user', 'highlight')
1274 item_list.sort()
1275 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001276 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001277 self.customlist.SetMenu(item_list, '- no custom themes -')
1278 else:
1279 self.customlist.SetMenu(item_list, item_list[0])
1280 # Revert to default theme.
1281 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1282 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1283 # User can't back out of these changes, they must be applied now.
1284 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001285 self.cd.save_all_changed_extensions()
1286 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001287 self.set_theme_type()
1288
1289
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001290class KeysPage(Frame):
1291
1292 def __init__(self, master):
1293 super().__init__(master)
1294 self.cd = master.master
1295 self.create_page_keys()
1296 self.load_key_cfg()
1297
1298 def create_page_keys(self):
1299 """Return frame of widgets for Keys tab.
1300
1301 Enable users to provisionally change both individual and sets of
1302 keybindings (shortcut keys). Except for features implemented as
1303 extensions, keybindings are stored in complete sets called
1304 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1305 as far as the dialog is concerned. Any keyset can be used as the
1306 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1307
1308 Function load_key_cfg() initializes tk variables and keyset
1309 lists and calls load_keys_list for the current keyset.
1310 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1311 keyset_source, which controls if the current set of keybindings
1312 are from a builtin or custom keyset. DynOptionMenus builtinlist
1313 and customlist contain lists of the builtin and custom keysets,
1314 respectively, and the current item from each list is stored in
1315 vars builtin_name and custom_name.
1316
1317 Button delete_custom_keys invokes delete_custom_keys() to delete
1318 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1319 save_custom_keys invokes save_as_new_key_set() which calls
1320 get_new_keys_name() and create_new_key_set() to save a custom keyset
1321 and its keybindings to idleConf.userCfg['keys'].
1322
1323 Listbox bindingslist contains all of the keybindings for the
1324 selected keyset. The keybindings are loaded in load_keys_list()
1325 and are pairs of (event, [keys]) where keys can be a list
1326 of one or more key combinations to bind to the same event.
1327 Mouse button 1 click invokes on_bindingslist_select(), which
1328 allows button_new_keys to be clicked.
1329
1330 So, an item is selected in listbindings, which activates
1331 button_new_keys, and clicking button_new_keys calls function
1332 get_new_keys(). Function get_new_keys() gets the key mappings from the
1333 current keyset for the binding event item that was selected. The
1334 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001335 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001336 to be entered for that binding event. If the keys aren't
1337 changed, nothing happens. If the keys are changed and the keyset
1338 is a builtin, function get_new_keys_name() will be called
1339 for input of a custom keyset name. If no name is given, then the
1340 change to the keybinding will abort and no updates will be made. If
1341 a custom name is entered in the prompt or if the current keyset was
1342 already custom (and thus didn't require a prompt), then
1343 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1344 with the change to the event binding. The item listing in bindingslist
1345 is updated with the new keys. Var keybinding is also set which invokes
1346 the callback function, var_changed_keybinding, to add the change to
1347 the 'keys' or 'extensions' changes tracker based on the binding type.
1348
1349 Tk Variables:
1350 keybinding: Action/key bindings.
1351
1352 Methods:
1353 load_keys_list: Reload active set.
1354 create_new_key_set: Combine active keyset and changes.
1355 set_keys_type: Command for keyset_source.
1356 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1357 deactivate_current_config: Remove keys bindings in editors.
1358
1359 Widgets for KeysPage(frame): (*) widgets bound to self
1360 frame_key_sets: LabelFrame
1361 frames[0]: Frame
1362 (*)builtin_keyset_on: Radiobutton - var keyset_source
1363 (*)custom_keyset_on: Radiobutton - var keyset_source
1364 (*)builtinlist: DynOptionMenu - var builtin_name,
1365 func keybinding_selected
1366 (*)customlist: DynOptionMenu - var custom_name,
1367 func keybinding_selected
1368 (*)keys_message: Label
1369 frames[1]: Frame
1370 (*)button_delete_custom_keys: Button - delete_custom_keys
1371 (*)button_save_custom_keys: Button - save_as_new_key_set
1372 frame_custom: LabelFrame
1373 frame_target: Frame
1374 target_title: Label
1375 scroll_target_y: Scrollbar
1376 scroll_target_x: Scrollbar
1377 (*)bindingslist: ListBox - on_bindingslist_select
1378 (*)button_new_keys: Button - get_new_keys & ..._name
1379 """
1380 self.builtin_name = tracers.add(
1381 StringVar(self), self.var_changed_builtin_name)
1382 self.custom_name = tracers.add(
1383 StringVar(self), self.var_changed_custom_name)
1384 self.keyset_source = tracers.add(
1385 BooleanVar(self), self.var_changed_keyset_source)
1386 self.keybinding = tracers.add(
1387 StringVar(self), self.var_changed_keybinding)
1388
1389 # Create widgets:
1390 # body and section frames.
1391 frame_custom = LabelFrame(
1392 self, borderwidth=2, relief=GROOVE,
1393 text=' Custom Key Bindings ')
1394 frame_key_sets = LabelFrame(
1395 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1396 # frame_custom.
1397 frame_target = Frame(frame_custom)
1398 target_title = Label(frame_target, text='Action - Key(s)')
1399 scroll_target_y = Scrollbar(frame_target)
1400 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1401 self.bindingslist = Listbox(
1402 frame_target, takefocus=FALSE, exportselection=FALSE)
1403 self.bindingslist.bind('<ButtonRelease-1>',
1404 self.on_bindingslist_select)
1405 scroll_target_y['command'] = self.bindingslist.yview
1406 scroll_target_x['command'] = self.bindingslist.xview
1407 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1408 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1409 self.button_new_keys = Button(
1410 frame_custom, text='Get New Keys for Selection',
1411 command=self.get_new_keys, state=DISABLED)
1412 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001413 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001414 for i in range(2)]
1415 self.builtin_keyset_on = Radiobutton(
1416 frames[0], variable=self.keyset_source, value=1,
1417 command=self.set_keys_type, text='Use a Built-in Key Set')
1418 self.custom_keyset_on = Radiobutton(
1419 frames[0], variable=self.keyset_source, value=0,
1420 command=self.set_keys_type, text='Use a Custom Key Set')
1421 self.builtinlist = DynOptionMenu(
1422 frames[0], self.builtin_name, None, command=None)
1423 self.customlist = DynOptionMenu(
1424 frames[0], self.custom_name, None, command=None)
1425 self.button_delete_custom_keys = Button(
1426 frames[1], text='Delete Custom Key Set',
1427 command=self.delete_custom_keys)
1428 self.button_save_custom_keys = Button(
1429 frames[1], text='Save as New Custom Key Set',
1430 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001431 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001432
1433 # Pack widgets:
1434 # body.
1435 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1436 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1437 # frame_custom.
1438 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1439 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1440 # frame_target.
1441 frame_target.columnconfigure(0, weight=1)
1442 frame_target.rowconfigure(1, weight=1)
1443 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1444 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1445 scroll_target_y.grid(row=1, column=1, sticky=NS)
1446 scroll_target_x.grid(row=2, column=0, sticky=EW)
1447 # frame_key_sets.
1448 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1449 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1450 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1451 self.customlist.grid(row=1, column=1, sticky=NSEW)
1452 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1453 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1454 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1455 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1456 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1457
1458 def load_key_cfg(self):
1459 "Load current configuration settings for the keybinding options."
1460 # Set current keys type radiobutton.
1461 self.keyset_source.set(idleConf.GetOption(
1462 'main', 'Keys', 'default', type='bool', default=1))
1463 # Set current keys.
1464 current_option = idleConf.CurrentKeys()
1465 # Load available keyset option menus.
1466 if self.keyset_source.get(): # Default theme selected.
1467 item_list = idleConf.GetSectionList('default', 'keys')
1468 item_list.sort()
1469 self.builtinlist.SetMenu(item_list, current_option)
1470 item_list = idleConf.GetSectionList('user', 'keys')
1471 item_list.sort()
1472 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001473 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001474 self.custom_name.set('- no custom keys -')
1475 else:
1476 self.customlist.SetMenu(item_list, item_list[0])
1477 else: # User key set selected.
1478 item_list = idleConf.GetSectionList('user', 'keys')
1479 item_list.sort()
1480 self.customlist.SetMenu(item_list, current_option)
1481 item_list = idleConf.GetSectionList('default', 'keys')
1482 item_list.sort()
1483 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1484 self.set_keys_type()
1485 # Load keyset element list.
1486 keyset_name = idleConf.CurrentKeys()
1487 self.load_keys_list(keyset_name)
1488
1489 def var_changed_builtin_name(self, *params):
1490 "Process selection of builtin key set."
1491 old_keys = (
1492 'IDLE Classic Windows',
1493 'IDLE Classic Unix',
1494 'IDLE Classic Mac',
1495 'IDLE Classic OSX',
1496 )
1497 value = self.builtin_name.get()
1498 if value not in old_keys:
1499 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1500 changes.add_option('main', 'Keys', 'name', old_keys[0])
1501 changes.add_option('main', 'Keys', 'name2', value)
1502 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001503 else:
1504 changes.add_option('main', 'Keys', 'name', value)
1505 changes.add_option('main', 'Keys', 'name2', '')
1506 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001507 self.load_keys_list(value)
1508
1509 def var_changed_custom_name(self, *params):
1510 "Process selection of custom key set."
1511 value = self.custom_name.get()
1512 if value != '- no custom keys -':
1513 changes.add_option('main', 'Keys', 'name', value)
1514 self.load_keys_list(value)
1515
1516 def var_changed_keyset_source(self, *params):
1517 "Process toggle between builtin key set and custom key set."
1518 value = self.keyset_source.get()
1519 changes.add_option('main', 'Keys', 'default', value)
1520 if value:
1521 self.var_changed_builtin_name()
1522 else:
1523 self.var_changed_custom_name()
1524
1525 def var_changed_keybinding(self, *params):
1526 "Store change to a keybinding."
1527 value = self.keybinding.get()
1528 key_set = self.custom_name.get()
1529 event = self.bindingslist.get(ANCHOR).split()[0]
1530 if idleConf.IsCoreBinding(event):
1531 changes.add_option('keys', key_set, event, value)
1532 else: # Event is an extension binding.
1533 ext_name = idleConf.GetExtnNameForEvent(event)
1534 ext_keybind_section = ext_name + '_cfgBindings'
1535 changes.add_option('extensions', ext_keybind_section, event, value)
1536
1537 def set_keys_type(self):
1538 "Set available screen options based on builtin or custom key set."
1539 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001540 self.builtinlist['state'] = 'normal'
1541 self.customlist['state'] = 'disabled'
1542 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001543 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001544 self.builtinlist['state'] = 'disabled'
1545 self.custom_keyset_on.state(('!disabled',))
1546 self.customlist['state'] = 'normal'
1547 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001548
1549 def get_new_keys(self):
1550 """Handle event to change key binding for selected line.
1551
1552 A selection of a key/binding in the list of current
1553 bindings pops up a dialog to enter a new binding. If
1554 the current key set is builtin and a binding has
1555 changed, then a name for a custom key set needs to be
1556 entered for the change to be applied.
1557 """
1558 list_index = self.bindingslist.index(ANCHOR)
1559 binding = self.bindingslist.get(list_index)
1560 bind_name = binding.split()[0]
1561 if self.keyset_source.get():
1562 current_key_set_name = self.builtin_name.get()
1563 else:
1564 current_key_set_name = self.custom_name.get()
1565 current_bindings = idleConf.GetCurrentKeySet()
1566 if current_key_set_name in changes['keys']: # unsaved changes
1567 key_set_changes = changes['keys'][current_key_set_name]
1568 for event in key_set_changes:
1569 current_bindings[event] = key_set_changes[event].split()
1570 current_key_sequences = list(current_bindings.values())
1571 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1572 current_key_sequences).result
1573 if new_keys:
1574 if self.keyset_source.get(): # Current key set is a built-in.
1575 message = ('Your changes will be saved as a new Custom Key Set.'
1576 ' Enter a name for your new Custom Key Set below.')
1577 new_keyset = self.get_new_keys_name(message)
1578 if not new_keyset: # User cancelled custom key set creation.
1579 self.bindingslist.select_set(list_index)
1580 self.bindingslist.select_anchor(list_index)
1581 return
1582 else: # Create new custom key set based on previously active key set.
1583 self.create_new_key_set(new_keyset)
1584 self.bindingslist.delete(list_index)
1585 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1586 self.bindingslist.select_set(list_index)
1587 self.bindingslist.select_anchor(list_index)
1588 self.keybinding.set(new_keys)
1589 else:
1590 self.bindingslist.select_set(list_index)
1591 self.bindingslist.select_anchor(list_index)
1592
1593 def get_new_keys_name(self, message):
1594 "Return new key set name from query popup."
1595 used_names = (idleConf.GetSectionList('user', 'keys') +
1596 idleConf.GetSectionList('default', 'keys'))
1597 new_keyset = SectionName(
1598 self, 'New Custom Key Set', message, used_names).result
1599 return new_keyset
1600
1601 def save_as_new_key_set(self):
1602 "Prompt for name of new key set and save changes using that name."
1603 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1604 if new_keys_name:
1605 self.create_new_key_set(new_keys_name)
1606
1607 def on_bindingslist_select(self, event):
1608 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001609 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001610
1611 def create_new_key_set(self, new_key_set_name):
1612 """Create a new custom key set with the given name.
1613
1614 Copy the bindings/keys from the previously active keyset
1615 to the new keyset and activate the new custom keyset.
1616 """
1617 if self.keyset_source.get():
1618 prev_key_set_name = self.builtin_name.get()
1619 else:
1620 prev_key_set_name = self.custom_name.get()
1621 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1622 new_keys = {}
1623 for event in prev_keys: # Add key set to changed items.
1624 event_name = event[2:-2] # Trim off the angle brackets.
1625 binding = ' '.join(prev_keys[event])
1626 new_keys[event_name] = binding
1627 # Handle any unsaved changes to prev key set.
1628 if prev_key_set_name in changes['keys']:
1629 key_set_changes = changes['keys'][prev_key_set_name]
1630 for event in key_set_changes:
1631 new_keys[event] = key_set_changes[event]
1632 # Save the new key set.
1633 self.save_new_key_set(new_key_set_name, new_keys)
1634 # Change GUI over to the new key set.
1635 custom_key_list = idleConf.GetSectionList('user', 'keys')
1636 custom_key_list.sort()
1637 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1638 self.keyset_source.set(0)
1639 self.set_keys_type()
1640
1641 def load_keys_list(self, keyset_name):
1642 """Reload the list of action/key binding pairs for the active key set.
1643
1644 An action/key binding can be selected to change the key binding.
1645 """
1646 reselect = False
1647 if self.bindingslist.curselection():
1648 reselect = True
1649 list_index = self.bindingslist.index(ANCHOR)
1650 keyset = idleConf.GetKeySet(keyset_name)
1651 bind_names = list(keyset.keys())
1652 bind_names.sort()
1653 self.bindingslist.delete(0, END)
1654 for bind_name in bind_names:
1655 key = ' '.join(keyset[bind_name])
1656 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1657 if keyset_name in changes['keys']:
1658 # Handle any unsaved changes to this key set.
1659 if bind_name in changes['keys'][keyset_name]:
1660 key = changes['keys'][keyset_name][bind_name]
1661 self.bindingslist.insert(END, bind_name+' - '+key)
1662 if reselect:
1663 self.bindingslist.see(list_index)
1664 self.bindingslist.select_set(list_index)
1665 self.bindingslist.select_anchor(list_index)
1666
1667 @staticmethod
1668 def save_new_key_set(keyset_name, keyset):
1669 """Save a newly created core key set.
1670
1671 Add keyset to idleConf.userCfg['keys'], not to disk.
1672 If the keyset doesn't exist, it is created. The
1673 binding/keys are taken from the keyset argument.
1674
1675 keyset_name - string, the name of the new key set
1676 keyset - dictionary containing the new keybindings
1677 """
1678 if not idleConf.userCfg['keys'].has_section(keyset_name):
1679 idleConf.userCfg['keys'].add_section(keyset_name)
1680 for event in keyset:
1681 value = keyset[event]
1682 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1683
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001684 def askyesno(self, *args, **kwargs):
1685 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001686 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001687
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001688 def delete_custom_keys(self):
1689 """Handle event to delete a custom key set.
1690
1691 Applying the delete deactivates the current configuration and
1692 reverts to the default. The custom key set is permanently
1693 deleted from the config file.
1694 """
1695 keyset_name = self.custom_name.get()
1696 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001697 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001698 'Delete Key Set', delmsg % keyset_name, parent=self):
1699 return
1700 self.cd.deactivate_current_config()
1701 # Remove key set from changes, config, and file.
1702 changes.delete_section('keys', keyset_name)
1703 # Reload user key set list.
1704 item_list = idleConf.GetSectionList('user', 'keys')
1705 item_list.sort()
1706 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001707 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001708 self.customlist.SetMenu(item_list, '- no custom keys -')
1709 else:
1710 self.customlist.SetMenu(item_list, item_list[0])
1711 # Revert to default key set.
1712 self.keyset_source.set(idleConf.defaultCfg['main']
1713 .Get('Keys', 'default'))
1714 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1715 or idleConf.default_keys())
1716 # User can't back out of these changes, they must be applied now.
1717 changes.save_all()
1718 self.cd.save_all_changed_extensions()
1719 self.cd.activate_config_changes()
1720 self.set_keys_type()
1721
1722
csabellae8eb17b2017-07-30 18:39:17 -04001723class GenPage(Frame):
1724
csabella6f446be2017-08-01 00:24:07 -04001725 def __init__(self, master):
1726 super().__init__(master)
csabellae8eb17b2017-07-30 18:39:17 -04001727 self.create_page_general()
1728 self.load_general_cfg()
1729
1730 def create_page_general(self):
1731 """Return frame of widgets for General tab.
1732
1733 Enable users to provisionally change general options. Function
1734 load_general_cfg intializes tk variables and helplist using
1735 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1736 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1737 set var autosave. Entry boxes win_width_int and win_height_int
1738 set var win_width and win_height. Setting var_name invokes the
1739 default callback that adds option to changes.
1740
1741 Helplist: load_general_cfg loads list user_helplist with
1742 name, position pairs and copies names to listbox helplist.
1743 Clicking a name invokes help_source selected. Clicking
1744 button_helplist_name invokes helplist_item_name, which also
1745 changes user_helplist. These functions all call
1746 set_add_delete_state. All but load call update_help_changes to
1747 rewrite changes['main']['HelpFiles'].
1748
Cheryl Sabella2f896462017-08-14 21:21:43 -04001749 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001750 frame_window: LabelFrame
1751 frame_run: Frame
1752 startup_title: Label
1753 (*)startup_editor_on: Radiobutton - startup_edit
1754 (*)startup_shell_on: Radiobutton - startup_edit
1755 frame_win_size: Frame
1756 win_size_title: Label
1757 win_width_title: Label
1758 (*)win_width_int: Entry - win_width
1759 win_height_title: Label
1760 (*)win_height_int: Entry - win_height
1761 frame_editor: LabelFrame
1762 frame_save: Frame
1763 run_save_title: Label
1764 (*)save_ask_on: Radiobutton - autosave
1765 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella2f896462017-08-14 21:21:43 -04001766 frame_help: LabelFrame
1767 frame_helplist: Frame
1768 frame_helplist_buttons: Frame
1769 (*)button_helplist_edit
1770 (*)button_helplist_add
1771 (*)button_helplist_remove
1772 (*)helplist: ListBox
1773 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001774 """
wohlganger58fc71c2017-09-10 16:19:47 -05001775 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001776 self.startup_edit = tracers.add(
1777 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001778 self.win_width = tracers.add(
1779 StringVar(self), ('main', 'EditorWindow', 'width'))
1780 self.win_height = tracers.add(
1781 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001782 self.autocomplete_wait = tracers.add(
1783 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1784 self.paren_style = tracers.add(
1785 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1786 self.flash_delay = tracers.add(
1787 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1788 self.paren_bell = tracers.add(
1789 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001790
wohlganger58fc71c2017-09-10 16:19:47 -05001791 self.autosave = tracers.add(
1792 IntVar(self), ('main', 'General', 'autosave'))
1793 self.format_width = tracers.add(
1794 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
1795 self.context_lines = tracers.add(
1796 StringVar(self), ('extensions', 'CodeContext', 'numlines'))
1797
1798 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001799 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001800 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1801 text=' Window Preferences')
1802 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1803 text=' Editor Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001804 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1805 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001806 # Frame_window.
1807 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001808 startup_title = Label(frame_run, text='At Startup')
1809 self.startup_editor_on = Radiobutton(
1810 frame_run, variable=self.startup_edit, value=1,
1811 text="Open Edit Window")
1812 self.startup_shell_on = Radiobutton(
1813 frame_run, variable=self.startup_edit, value=0,
1814 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001815
wohlganger58fc71c2017-09-10 16:19:47 -05001816 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001817 win_size_title = Label(
1818 frame_win_size, text='Initial Window Size (in characters)')
1819 win_width_title = Label(frame_win_size, text='Width')
1820 self.win_width_int = Entry(
1821 frame_win_size, textvariable=self.win_width, width=3)
1822 win_height_title = Label(frame_win_size, text='Height')
1823 self.win_height_int = Entry(
1824 frame_win_size, textvariable=self.win_height, width=3)
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001825
wohlganger58fc71c2017-09-10 16:19:47 -05001826 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1827 auto_wait_title = Label(frame_autocomplete,
1828 text='Completions Popup Wait (milliseconds)')
1829 self.auto_wait_int = Entry(frame_autocomplete, width=6,
1830 textvariable=self.autocomplete_wait)
1831
1832 frame_paren1 = Frame(frame_window, borderwidth=0)
1833 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1834 self.paren_style_type = OptionMenu(
1835 frame_paren1, self.paren_style, 'expression',
1836 "opener","parens","expression")
1837 frame_paren2 = Frame(frame_window, borderwidth=0)
1838 paren_time_title = Label(
1839 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1840 '(0 is until next input)')
1841 self.paren_flash_time = Entry(
1842 frame_paren2, textvariable=self.flash_delay, width=6)
1843 self.bell_on = Checkbutton(
1844 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1845
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001846 # Frame_editor.
1847 frame_save = Frame(frame_editor, borderwidth=0)
1848 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1849 self.save_ask_on = Radiobutton(
1850 frame_save, variable=self.autosave, value=0,
1851 text="Prompt to Save")
1852 self.save_auto_on = Radiobutton(
1853 frame_save, variable=self.autosave, value=1,
1854 text='No Prompt')
1855
wohlganger58fc71c2017-09-10 16:19:47 -05001856 frame_format = Frame(frame_editor, borderwidth=0)
1857 format_width_title = Label(frame_format,
1858 text='Format Paragraph Max Width')
1859 self.format_width_int = Entry(
1860 frame_format, textvariable=self.format_width, width=4)
1861
1862 frame_context = Frame(frame_editor, borderwidth=0)
1863 context_title = Label(frame_context, text='Context Lines :')
1864 self.context_int = Entry(
1865 frame_context, textvariable=self.context_lines, width=3)
1866
1867
csabellae8eb17b2017-07-30 18:39:17 -04001868 # frame_help.
1869 frame_helplist = Frame(frame_help)
1870 frame_helplist_buttons = Frame(frame_helplist)
1871 self.helplist = Listbox(
1872 frame_helplist, height=5, takefocus=True,
1873 exportselection=FALSE)
1874 scroll_helplist = Scrollbar(frame_helplist)
1875 scroll_helplist['command'] = self.helplist.yview
1876 self.helplist['yscrollcommand'] = scroll_helplist.set
1877 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1878 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001879 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001880 width=8, command=self.helplist_item_edit)
1881 self.button_helplist_add = Button(
1882 frame_helplist_buttons, text='Add',
1883 width=8, command=self.helplist_item_add)
1884 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001885 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001886 width=8, command=self.helplist_item_remove)
1887
1888 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001889 # Body.
1890 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1891 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04001892 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1893 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001894 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001895 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1896 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1897 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04001898 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001899 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001900 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1901 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1902 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1903 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1904 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001905 # frame_autocomplete.
1906 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
1907 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1908 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
1909 # frame_paren.
1910 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
1911 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1912 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
1913 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
1914 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
1915 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1916 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
1917
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001918 # frame_save.
1919 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
1920 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1921 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1922 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001923 # frame_format.
1924 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
1925 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1926 self.format_width_int.pack(side=TOP, padx=10, pady=5)
1927 # frame_context.
1928 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
1929 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1930 self.context_int.pack(side=TOP, padx=5, pady=5)
1931
csabellae8eb17b2017-07-30 18:39:17 -04001932 # frame_help.
1933 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1934 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1935 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1936 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1937 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1938 self.button_helplist_add.pack(side=TOP, anchor=W)
1939 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1940
1941 def load_general_cfg(self):
1942 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05001943 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04001944 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05001945 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04001946 self.win_width.set(idleConf.GetOption(
1947 'main', 'EditorWindow', 'width', type='int'))
1948 self.win_height.set(idleConf.GetOption(
1949 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05001950 self.autocomplete_wait.set(idleConf.GetOption(
1951 'extensions', 'AutoComplete', 'popupwait', type='int'))
1952 self.paren_style.set(idleConf.GetOption(
1953 'extensions', 'ParenMatch', 'style'))
1954 self.flash_delay.set(idleConf.GetOption(
1955 'extensions', 'ParenMatch', 'flash-delay', type='int'))
1956 self.paren_bell.set(idleConf.GetOption(
1957 'extensions', 'ParenMatch', 'bell'))
1958
1959 # Set variables for editor windows.
1960 self.autosave.set(idleConf.GetOption(
1961 'main', 'General', 'autosave', default=0, type='bool'))
1962 self.format_width.set(idleConf.GetOption(
1963 'extensions', 'FormatParagraph', 'max-width', type='int'))
1964 self.context_lines.set(idleConf.GetOption(
1965 'extensions', 'CodeContext', 'numlines', type='int'))
1966
csabellae8eb17b2017-07-30 18:39:17 -04001967 # Set additional help sources.
1968 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
1969 self.helplist.delete(0, 'end')
1970 for help_item in self.user_helplist:
1971 self.helplist.insert(END, help_item[0])
1972 self.set_add_delete_state()
1973
1974 def help_source_selected(self, event):
1975 "Handle event for selecting additional help."
1976 self.set_add_delete_state()
1977
1978 def set_add_delete_state(self):
1979 "Toggle the state for the help list buttons based on list entries."
1980 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001981 self.button_helplist_edit.state(('disabled',))
1982 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04001983 else: # Some entries.
1984 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001985 self.button_helplist_edit.state(('!disabled',))
1986 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04001987 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001988 self.button_helplist_edit.state(('disabled',))
1989 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04001990
1991 def helplist_item_add(self):
1992 """Handle add button for the help list.
1993
1994 Query for name and location of new help sources and add
1995 them to the list.
1996 """
1997 help_source = HelpSource(self, 'New Help Source').result
1998 if help_source:
1999 self.user_helplist.append(help_source)
2000 self.helplist.insert(END, help_source[0])
2001 self.update_help_changes()
2002
2003 def helplist_item_edit(self):
2004 """Handle edit button for the help list.
2005
2006 Query with existing help source information and update
2007 config if the values are changed.
2008 """
2009 item_index = self.helplist.index(ANCHOR)
2010 help_source = self.user_helplist[item_index]
2011 new_help_source = HelpSource(
2012 self, 'Edit Help Source',
2013 menuitem=help_source[0],
2014 filepath=help_source[1],
2015 ).result
2016 if new_help_source and new_help_source != help_source:
2017 self.user_helplist[item_index] = new_help_source
2018 self.helplist.delete(item_index)
2019 self.helplist.insert(item_index, new_help_source[0])
2020 self.update_help_changes()
2021 self.set_add_delete_state() # Selected will be un-selected
2022
2023 def helplist_item_remove(self):
2024 """Handle remove button for the help list.
2025
2026 Delete the help list item from config.
2027 """
2028 item_index = self.helplist.index(ANCHOR)
2029 del(self.user_helplist[item_index])
2030 self.helplist.delete(item_index)
2031 self.update_help_changes()
2032 self.set_add_delete_state()
2033
2034 def update_help_changes(self):
2035 "Clear and rebuild the HelpFiles section in changes"
2036 changes['main']['HelpFiles'] = {}
2037 for num in range(1, len(self.user_helplist) + 1):
2038 changes.add_option(
2039 'main', 'HelpFiles', str(num),
2040 ';'.join(self.user_helplist[num-1][:2]))
2041
2042
csabella45bf7232017-07-26 19:09:58 -04002043class VarTrace:
2044 """Maintain Tk variables trace state."""
2045
2046 def __init__(self):
2047 """Store Tk variables and callbacks.
2048
2049 untraced: List of tuples (var, callback)
2050 that do not have the callback attached
2051 to the Tk var.
2052 traced: List of tuples (var, callback) where
2053 that callback has been attached to the var.
2054 """
2055 self.untraced = []
2056 self.traced = []
2057
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002058 def clear(self):
2059 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002060 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002061 self.untraced.clear()
2062 self.traced.clear()
2063
csabella45bf7232017-07-26 19:09:58 -04002064 def add(self, var, callback):
2065 """Add (var, callback) tuple to untraced list.
2066
2067 Args:
2068 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002069 callback: Either function name to be used as a callback
2070 or a tuple with IdleConf config-type, section, and
2071 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002072
2073 Return:
2074 Tk variable instance.
2075 """
2076 if isinstance(callback, tuple):
2077 callback = self.make_callback(var, callback)
2078 self.untraced.append((var, callback))
2079 return var
2080
2081 @staticmethod
2082 def make_callback(var, config):
2083 "Return default callback function to add values to changes instance."
2084 def default_callback(*params):
2085 "Add config values to changes instance."
2086 changes.add_option(*config, var.get())
2087 return default_callback
2088
2089 def attach(self):
2090 "Attach callback to all vars that are not traced."
2091 while self.untraced:
2092 var, callback = self.untraced.pop()
2093 var.trace_add('write', callback)
2094 self.traced.append((var, callback))
2095
2096 def detach(self):
2097 "Remove callback from traced vars."
2098 while self.traced:
2099 var, callback = self.traced.pop()
2100 var.trace_remove('write', var.trace_info()[0][1])
2101 self.untraced.append((var, callback))
2102
2103
csabella5b591542017-07-28 14:40:59 -04002104tracers = VarTrace()
2105
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002106help_common = '''\
2107When you click either the Apply or Ok buttons, settings in this
2108dialog that are different from IDLE's default are saved in
2109a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002110these changes apply to all versions of IDLE installed on this
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002111machine. Some do not take affect until IDLE is restarted.
2112[Cancel] only cancels changes made since the last save.
2113'''
2114help_pages = {
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002115 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002116Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002117The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002118be used with older IDLE releases if it is saved as a custom
2119theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002120''',
2121 'Keys': '''
2122Keys:
2123The IDLE Modern Unix key set is new in June 2016. It can only
2124be used with older IDLE releases if it is saved as a custom
2125key set, with a different name.
2126''',
wohlganger58fc71c2017-09-10 16:19:47 -05002127 'General': '''
2128General:
wohlgangerfae2c352017-06-27 21:36:23 -05002129
wohlganger58fc71c2017-09-10 16:19:47 -05002130AutoComplete: Popupwait is milleseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002131cursor movement, before popping up completion box. Key char is '.' after
2132identifier or a '/' (or '\\' on Windows) within a string.
2133
2134FormatParagraph: Max-width is max chars in lines after re-formatting.
2135Use with paragraphs in both strings and comment blocks.
2136
2137ParenMatch: Style indicates what is highlighted when closer is entered:
2138'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2139'expression' (default) - also everything in between. Flash-delay is how
2140long to highlight if cursor is not moved (0 means forever).
2141'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002142}
2143
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002144
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002145def is_int(s):
2146 "Return 's is blank or represents an int'"
2147 if not s:
2148 return True
2149 try:
2150 int(s)
2151 return True
2152 except ValueError:
2153 return False
2154
2155
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002156class VerticalScrolledFrame(Frame):
2157 """A pure Tkinter vertically scrollable frame.
2158
2159 * Use the 'interior' attribute to place widgets inside the scrollable frame
2160 * Construct and pack/place/grid normally
2161 * This frame only allows vertical scrolling
2162 """
2163 def __init__(self, parent, *args, **kw):
2164 Frame.__init__(self, parent, *args, **kw)
2165
csabella7eb58832017-07-04 21:30:58 -04002166 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002167 vscrollbar = Scrollbar(self, orient=VERTICAL)
2168 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002169 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002170 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002171 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2172 vscrollbar.config(command=canvas.yview)
2173
csabella7eb58832017-07-04 21:30:58 -04002174 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002175 canvas.xview_moveto(0)
2176 canvas.yview_moveto(0)
2177
csabella7eb58832017-07-04 21:30:58 -04002178 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002179 self.interior = interior = Frame(canvas)
2180 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2181
csabella7eb58832017-07-04 21:30:58 -04002182 # Track changes to the canvas and frame width and sync them,
2183 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002184 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002185 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002186 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2187 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002188 interior.bind('<Configure>', _configure_interior)
2189
2190 def _configure_canvas(event):
2191 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002192 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002193 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2194 canvas.bind('<Configure>', _configure_canvas)
2195
2196 return
2197
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002198
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002199if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002200 import unittest
2201 unittest.main('idlelib.idle_test.test_configdialog',
2202 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002203 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002204 run(ConfigDialog)