blob: 683c36e9776c28deb9bc778bfbdce480c58f8c48 [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 Reedyd0cadba2015-10-11 22:07:31 -040030
terryjreedy349abd92017-07-07 16:00:57 -040031changes = ConfigChanges()
32
csabella5b591542017-07-28 14:40:59 -040033
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000034class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -040035 """Config dialog for IDLE.
36 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000037
Terry Jan Reedycd567362014-10-17 01:31:35 -040038 def __init__(self, parent, title='', _htest=False, _utest=False):
csabella7eb58832017-07-04 21:30:58 -040039 """Show the tabbed dialog for user configuration.
40
csabella36329a42017-07-13 23:32:01 -040041 Args:
42 parent - parent of this dialog
43 title - string which is the title of this popup dialog
44 _htest - bool, change box location when running htest
45 _utest - bool, don't wait_window when running unittest
46
47 Note: Focus set on font page fontlist.
48
49 Methods:
50 create_widgets
51 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040052 """
Steven M. Gavad721c482001-07-31 10:46:53 +000053 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040054 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040055 if _htest:
56 parent.instance_dict = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080057 if not _utest:
58 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000059
Steven M. Gavad721c482001-07-31 10:46:53 +000060 self.configure(borderwidth=5)
Terry Jan Reedycd567362014-10-17 01:31:35 -040061 self.title(title or 'IDLE Preferences')
csabellabac7d332017-06-26 17:46:26 -040062 x = parent.winfo_rootx() + 20
63 y = parent.winfo_rooty() + (30 if not _htest else 150)
64 self.geometry(f'+{x}+{y}')
csabella7eb58832017-07-04 21:30:58 -040065 # Each theme element key is its display name.
66 # The first value of the tuple is the sample area tag name.
67 # The second value is the display name list sort index.
csabellabac7d332017-06-26 17:46:26 -040068 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040069 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000070 self.transient(parent)
csabellabac7d332017-06-26 17:46:26 -040071 self.protocol("WM_DELETE_WINDOW", self.cancel)
csabella9397e2a2017-07-30 13:34:25 -040072 self.fontpage.fontlist.focus_set()
csabella7eb58832017-07-04 21:30:58 -040073 # XXX Decide whether to keep or delete these key bindings.
74 # Key bindings for this dialog.
75 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
76 # self.bind('<Alt-a>', self.Apply) #apply changes, save
77 # self.bind('<F1>', self.Help) #context help
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040078 # Attach callbacks after loading config to avoid calling them.
csabella5b591542017-07-28 14:40:59 -040079 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000080
Terry Jan Reedycfa89502014-07-14 23:07:32 -040081 if not _utest:
Louie Lu9b622fb2017-07-14 08:35:48 +080082 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040083 self.wm_deiconify()
84 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000085
csabellabac7d332017-06-26 17:46:26 -040086 def create_widgets(self):
csabella36329a42017-07-13 23:32:01 -040087 """Create and place widgets for tabbed dialog.
88
89 Widgets Bound to self:
csabellae8eb17b2017-07-30 18:39:17 -040090 note: Notebook
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040091 highpage: HighPage
csabellae8eb17b2017-07-30 18:39:17 -040092 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -040093 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -040094 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -040095 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -040096
97 Methods:
csabella36329a42017-07-13 23:32:01 -040098 create_action_buttons
99 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400100 activate_config_changes: Tell editors to reload.
101 """
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400102 self.note = note = Notebook(self, width=450, height=450)
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400103 self.highpage = HighPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400104 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400105 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400106 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400107 self.extpage = self.create_page_extensions()
108 note.add(self.fontpage, text='Fonts/Tabs')
109 note.add(self.highpage, text='Highlights')
110 note.add(self.keyspage, text=' Keys ')
111 note.add(self.genpage, text=' General ')
112 note.add(self.extpage, text='Extensions')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400113 note.enable_traversal()
114 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400115 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400116
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400117 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -0400118 """Return frame of action buttons for dialog.
119
120 Methods:
121 ok
122 apply
123 cancel
124 help
125
126 Widget Structure:
127 outer: Frame
128 buttons: Frame
129 (no assignment): Button (ok)
130 (no assignment): Button (apply)
131 (no assignment): Button (cancel)
132 (no assignment): Button (help)
133 (no assignment): Frame
134 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400135 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400136 # Changing the default padding on OSX results in unreadable
csabella7eb58832017-07-04 21:30:58 -0400137 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400138 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000139 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400140 padding_args = {'padding': (6, 3)}
141 outer = Frame(self, padding=2)
142 buttons = Frame(outer, padding=2)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400143 for txt, cmd in (
csabellabac7d332017-06-26 17:46:26 -0400144 ('Ok', self.ok),
145 ('Apply', self.apply),
146 ('Cancel', self.cancel),
147 ('Help', self.help)):
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400148 Button(buttons, text=txt, command=cmd, takefocus=FALSE,
csabellabac7d332017-06-26 17:46:26 -0400149 **padding_args).pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -0400150 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400151 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
152 buttons.pack(side=BOTTOM)
153 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400154
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400155 def ok(self):
156 """Apply config changes, then dismiss dialog.
157
158 Methods:
159 apply
160 destroy: inherited
161 """
162 self.apply()
163 self.destroy()
164
165 def apply(self):
166 """Apply config changes and leave dialog open.
167
168 Methods:
169 deactivate_current_config
170 save_all_changed_extensions
171 activate_config_changes
172 """
173 self.deactivate_current_config()
174 changes.save_all()
175 self.save_all_changed_extensions()
176 self.activate_config_changes()
177
178 def cancel(self):
179 """Dismiss config dialog.
180
181 Methods:
182 destroy: inherited
183 """
184 self.destroy()
185
186 def help(self):
187 """Create textview for config dialog help.
188
189 Attrbutes accessed:
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400190 note
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400191
192 Methods:
193 view_text: Method from textview module.
194 """
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400195 page = self.note.tab(self.note.select(), option='text').strip()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400196 view_text(self, title='Help for IDLE preferences',
197 text=help_common+help_pages.get(page, ''))
198
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400199 def deactivate_current_config(self):
200 """Remove current key bindings.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400201 Iterate over window instances defined in parent and remove
202 the keybindings.
203 """
204 # Before a config is saved, some cleanup of current
205 # config must be done - remove the previous keybindings.
206 win_instances = self.parent.instance_dict.keys()
207 for instance in win_instances:
208 instance.RemoveKeybindings()
209
210 def activate_config_changes(self):
211 """Apply configuration changes to current windows.
212
213 Dynamically update the current parent window instances
214 with some of the configuration changes.
215 """
216 win_instances = self.parent.instance_dict.keys()
217 for instance in win_instances:
218 instance.ResetColorizer()
219 instance.ResetFont()
220 instance.set_notabs_indentwidth()
221 instance.ApplyKeybindings()
222 instance.reset_help_menu_entries()
223
csabellabac7d332017-06-26 17:46:26 -0400224 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400225 """Part of the config dialog used for configuring IDLE extensions.
226
227 This code is generic - it works for any and all IDLE extensions.
228
229 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400230 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400231 GUI interface to change the configuration values, and saves the
232 changes using idleConf.
233
234 Not all changes take effect immediately - some may require restarting IDLE.
235 This depends on each extension's implementation.
236
237 All values are treated as text, and it is up to the user to supply
238 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300239 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -0400240
241 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300242 load_extensions:
csabella36329a42017-07-13 23:32:01 -0400243 extension_selected: Handle selection from list.
244 create_extension_frame: Hold widgets for one extension.
245 set_extension_value: Set in userCfg['extensions'].
246 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400247 """
248 parent = self.parent
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400249 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400250 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
251 self.ext_userCfg = idleConf.userCfg['extensions']
252 self.is_int = self.register(is_int)
253 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -0400254 # Create widgets - a listbox shows all available extensions, with the
255 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400256 self.extension_names = StringVar(self)
257 frame.rowconfigure(0, weight=1)
258 frame.columnconfigure(2, weight=1)
259 self.extension_list = Listbox(frame, listvariable=self.extension_names,
260 selectmode='browse')
261 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
262 scroll = Scrollbar(frame, command=self.extension_list.yview)
263 self.extension_list.yscrollcommand=scroll.set
264 self.details_frame = LabelFrame(frame, width=250, height=250)
265 self.extension_list.grid(column=0, row=0, sticky='nws')
266 scroll.grid(column=1, row=0, sticky='ns')
267 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
Cheryl Sabella7028e592017-08-26 14:26:02 -0400268 frame.configure(padding=10)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400269 self.config_frame = {}
270 self.current_extension = None
271
272 self.outerframe = self # TEMPORARY
273 self.tabbed_page_set = self.extension_list # TEMPORARY
274
csabella7eb58832017-07-04 21:30:58 -0400275 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400276 ext_names = ''
277 for ext_name in sorted(self.extensions):
278 self.create_extension_frame(ext_name)
279 ext_names = ext_names + '{' + ext_name + '} '
280 self.extension_names.set(ext_names)
281 self.extension_list.selection_set(0)
282 self.extension_selected(None)
283
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400284 return frame
285
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400286 def load_extensions(self):
287 "Fill self.extensions with data from the default and user configs."
288 self.extensions = {}
289 for ext_name in idleConf.GetExtensions(active_only=False):
wohlganger58fc71c2017-09-10 16:19:47 -0500290 # Former built-in extensions are already filtered out.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400291 self.extensions[ext_name] = []
292
293 for ext_name in self.extensions:
294 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
295
csabella7eb58832017-07-04 21:30:58 -0400296 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400297 enables = [opt_name for opt_name in opt_list
298 if opt_name.startswith('enable')]
299 for opt_name in enables:
300 opt_list.remove(opt_name)
301 opt_list = enables + opt_list
302
303 for opt_name in opt_list:
304 def_str = self.ext_defaultCfg.Get(
305 ext_name, opt_name, raw=True)
306 try:
307 def_obj = {'True':True, 'False':False}[def_str]
308 opt_type = 'bool'
309 except KeyError:
310 try:
311 def_obj = int(def_str)
312 opt_type = 'int'
313 except ValueError:
314 def_obj = def_str
315 opt_type = None
316 try:
317 value = self.ext_userCfg.Get(
318 ext_name, opt_name, type=opt_type, raw=True,
319 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -0400320 except ValueError: # Need this until .Get fixed.
321 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400322 var = StringVar(self)
323 var.set(str(value))
324
325 self.extensions[ext_name].append({'name': opt_name,
326 'type': opt_type,
327 'default': def_str,
328 'value': value,
329 'var': var,
330 })
331
332 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -0400333 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400334 newsel = self.extension_list.curselection()
335 if newsel:
336 newsel = self.extension_list.get(newsel)
337 if newsel is None or newsel != self.current_extension:
338 if self.current_extension:
339 self.details_frame.config(text='')
340 self.config_frame[self.current_extension].grid_forget()
341 self.current_extension = None
342 if newsel:
343 self.details_frame.config(text=newsel)
344 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
345 self.current_extension = newsel
346
347 def create_extension_frame(self, ext_name):
348 """Create a frame holding the widgets to configure one extension"""
349 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
350 self.config_frame[ext_name] = f
351 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -0400352 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400353 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -0400354 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400355 label = Label(entry_area, text=opt['name'])
356 label.grid(row=row, column=0, sticky=NW)
357 var = opt['var']
358 if opt['type'] == 'bool':
Cheryl Sabella7028e592017-08-26 14:26:02 -0400359 Checkbutton(entry_area, variable=var,
360 onvalue='True', offvalue='False', width=8
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400361 ).grid(row=row, column=1, sticky=W, padx=7)
362 elif opt['type'] == 'int':
363 Entry(entry_area, textvariable=var, validate='key',
364 validatecommand=(self.is_int, '%P')
365 ).grid(row=row, column=1, sticky=NSEW, padx=7)
366
367 else:
368 Entry(entry_area, textvariable=var
369 ).grid(row=row, column=1, sticky=NSEW, padx=7)
370 return
371
372 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400373 """Return True if the configuration was added or changed.
374
375 If the value is the same as the default, then remove it
376 from user config file.
377 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400378 name = opt['name']
379 default = opt['default']
380 value = opt['var'].get().strip() or default
381 opt['var'].set(value)
382 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400383 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400384 if (value == default):
385 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400386 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400387 return self.ext_userCfg.SetOption(section, name, value)
388
389 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400390 """Save configuration changes to the user config file.
391
392 Attributes accessed:
393 extensions
394
395 Methods:
396 set_extension_value
397 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400398 has_changes = False
399 for ext_name in self.extensions:
400 options = self.extensions[ext_name]
401 for opt in options:
402 if self.set_extension_value(ext_name, opt):
403 has_changes = True
404 if has_changes:
405 self.ext_userCfg.Save()
406
407
csabella6f446be2017-08-01 00:24:07 -0400408# class TabPage(Frame): # A template for Page classes.
409# def __init__(self, master):
410# super().__init__(master)
411# self.create_page_tab()
412# self.load_tab_cfg()
413# def create_page_tab(self):
414# # Define tk vars and register var and callback with tracers.
415# # Create subframes and widgets.
416# # Pack widgets.
417# def load_tab_cfg(self):
418# # Initialize widgets with data from idleConf.
419# def var_changed_var_name():
420# # For each tk var that needs other than default callback.
421# def other_methods():
422# # Define tab-specific behavior.
423
424
csabella9397e2a2017-07-30 13:34:25 -0400425class FontPage(Frame):
426
csabella6f446be2017-08-01 00:24:07 -0400427 def __init__(self, master, highpage):
428 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400429 self.highlight_sample = highpage.highlight_sample
430 self.create_page_font_tab()
431 self.load_font_cfg()
432 self.load_tab_cfg()
433
434 def create_page_font_tab(self):
435 """Return frame of widgets for Font/Tabs tab.
436
437 Fonts: Enable users to provisionally change font face, size, or
438 boldness and to see the consequence of proposed choices. Each
439 action set 3 options in changes structuree and changes the
440 corresponding aspect of the font sample on this page and
441 highlight sample on highlight page.
442
443 Function load_font_cfg initializes font vars and widgets from
444 idleConf entries and tk.
445
446 Fontlist: mouse button 1 click or up or down key invoke
447 on_fontlist_select(), which sets var font_name.
448
449 Sizelist: clicking the menubutton opens the dropdown menu. A
450 mouse button 1 click or return key sets var font_size.
451
452 Bold_toggle: clicking the box toggles var font_bold.
453
454 Changing any of the font vars invokes var_changed_font, which
455 adds all 3 font options to changes and calls set_samples.
456 Set_samples applies a new font constructed from the font vars to
457 font_sample and to highlight_sample on the hightlight page.
458
459 Tabs: Enable users to change spaces entered for indent tabs.
460 Changing indent_scale value with the mouse sets Var space_num,
461 which invokes the default callback to add an entry to
462 changes. Load_tab_cfg initializes space_num to default.
463
Cheryl Sabella2f896462017-08-14 21:21:43 -0400464 Widgets for FontPage(Frame): (*) widgets bound to self
465 frame_font: LabelFrame
466 frame_font_name: Frame
467 font_name_title: Label
468 (*)fontlist: ListBox - font_name
469 scroll_font: Scrollbar
470 frame_font_param: Frame
471 font_size_title: Label
472 (*)sizelist: DynOptionMenu - font_size
473 (*)bold_toggle: Checkbutton - font_bold
474 frame_font_sample: Frame
475 (*)font_sample: Label
476 frame_indent: LabelFrame
477 indent_title: Label
478 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400479 """
csabella6f446be2017-08-01 00:24:07 -0400480 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
481 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
482 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400483 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
484
485 # Create widgets:
486 # body and body section frames.
csabella9397e2a2017-07-30 13:34:25 -0400487 frame_font = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400488 self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
csabella9397e2a2017-07-30 13:34:25 -0400489 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400490 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400491 # frame_font.
492 frame_font_name = Frame(frame_font)
493 frame_font_param = Frame(frame_font)
494 font_name_title = Label(
495 frame_font_name, justify=LEFT, text='Font Face :')
496 self.fontlist = Listbox(frame_font_name, height=5,
497 takefocus=True, exportselection=FALSE)
498 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
499 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
500 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
501 scroll_font = Scrollbar(frame_font_name)
502 scroll_font.config(command=self.fontlist.yview)
503 self.fontlist.config(yscrollcommand=scroll_font.set)
504 font_size_title = Label(frame_font_param, text='Size :')
505 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
506 self.bold_toggle = Checkbutton(
507 frame_font_param, variable=self.font_bold,
508 onvalue=1, offvalue=0, text='Bold')
509 frame_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1)
csabella6f446be2017-08-01 00:24:07 -0400510 temp_font = tkFont.Font(self, ('courier', 10, 'normal'))
csabella9397e2a2017-07-30 13:34:25 -0400511 self.font_sample = Label(
512 frame_font_sample, justify=LEFT, font=temp_font,
513 text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]')
514 # frame_indent.
515 indent_title = Label(
516 frame_indent, justify=LEFT,
517 text='Python Standard: 4 Spaces!')
518 self.indent_scale = Scale(
519 frame_indent, variable=self.space_num,
520 orient='horizontal', tickinterval=2, from_=2, to=16)
521
522 # Pack widgets:
523 # body.
524 frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
525 frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y)
526 # frame_font.
527 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
528 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
529 font_name_title.pack(side=TOP, anchor=W)
530 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
531 scroll_font.pack(side=LEFT, fill=Y)
532 font_size_title.pack(side=LEFT, anchor=W)
533 self.sizelist.pack(side=LEFT, anchor=W)
534 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
535 frame_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
536 self.font_sample.pack(expand=TRUE, fill=BOTH)
537 # frame_indent.
538 frame_indent.pack(side=TOP, fill=X)
539 indent_title.pack(side=TOP, anchor=W, padx=5)
540 self.indent_scale.pack(side=TOP, padx=5, fill=X)
541
csabella9397e2a2017-07-30 13:34:25 -0400542 def load_font_cfg(self):
543 """Load current configuration settings for the font options.
544
545 Retrieve current font with idleConf.GetFont and font families
546 from tk. Setup fontlist and set font_name. Setup sizelist,
547 which sets font_size. Set font_bold. Call set_samples.
548 """
549 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
550 font_name = configured_font[0].lower()
551 font_size = configured_font[1]
552 font_bold = configured_font[2]=='bold'
553
554 # Set editor font selection list and font_name.
555 fonts = list(tkFont.families(self))
556 fonts.sort()
557 for font in fonts:
558 self.fontlist.insert(END, font)
559 self.font_name.set(font_name)
560 lc_fonts = [s.lower() for s in fonts]
561 try:
562 current_font_index = lc_fonts.index(font_name)
563 self.fontlist.see(current_font_index)
564 self.fontlist.select_set(current_font_index)
565 self.fontlist.select_anchor(current_font_index)
566 self.fontlist.activate(current_font_index)
567 except ValueError:
568 pass
569 # Set font size dropdown.
570 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
571 '16', '18', '20', '22', '25', '29', '34', '40'),
572 font_size)
573 # Set font weight.
574 self.font_bold.set(font_bold)
575 self.set_samples()
576
577 def var_changed_font(self, *params):
578 """Store changes to font attributes.
579
580 When one font attribute changes, save them all, as they are
581 not independent from each other. In particular, when we are
582 overriding the default font, we need to write out everything.
583 """
584 value = self.font_name.get()
585 changes.add_option('main', 'EditorWindow', 'font', value)
586 value = self.font_size.get()
587 changes.add_option('main', 'EditorWindow', 'font-size', value)
588 value = self.font_bold.get()
589 changes.add_option('main', 'EditorWindow', 'font-bold', value)
590 self.set_samples()
591
592 def on_fontlist_select(self, event):
593 """Handle selecting a font from the list.
594
595 Event can result from either mouse click or Up or Down key.
596 Set font_name and example displays to selection.
597 """
598 font = self.fontlist.get(
599 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
600 self.font_name.set(font.lower())
601
602 def set_samples(self, event=None):
603 """Update update both screen samples with the font settings.
604
605 Called on font initialization and change events.
606 Accesses font_name, font_size, and font_bold Variables.
607 Updates font_sample and hightlight page highlight_sample.
608 """
609 font_name = self.font_name.get()
610 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
611 new_font = (font_name, self.font_size.get(), font_weight)
612 self.font_sample['font'] = new_font
613 self.highlight_sample['font'] = new_font
614
615 def load_tab_cfg(self):
616 """Load current configuration settings for the tab options.
617
618 Attributes updated:
619 space_num: Set to value from idleConf.
620 """
621 # Set indent sizes.
622 space_num = idleConf.GetOption(
623 'main', 'Indent', 'num-spaces', default=4, type='int')
624 self.space_num.set(space_num)
625
626 def var_changed_space_num(self, *params):
627 "Store change to indentation size."
628 value = self.space_num.get()
629 changes.add_option('main', 'Indent', 'num-spaces', value)
630
631
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400632class HighPage(Frame):
633
634 def __init__(self, master):
635 super().__init__(master)
636 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400637 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400638 self.create_page_highlight()
639 self.load_theme_cfg()
640
641 def create_page_highlight(self):
642 """Return frame of widgets for Highlighting tab.
643
644 Enable users to provisionally change foreground and background
645 colors applied to textual tags. Color mappings are stored in
646 complete listings called themes. Built-in themes in
647 idlelib/config-highlight.def are fixed as far as the dialog is
648 concerned. Any theme can be used as the base for a new custom
649 theme, stored in .idlerc/config-highlight.cfg.
650
651 Function load_theme_cfg() initializes tk variables and theme
652 lists and calls paint_theme_sample() and set_highlight_target()
653 for the current theme. Radiobuttons builtin_theme_on and
654 custom_theme_on toggle var theme_source, which controls if the
655 current set of colors are from a builtin or custom theme.
656 DynOptionMenus builtinlist and customlist contain lists of the
657 builtin and custom themes, respectively, and the current item
658 from each list is stored in vars builtin_name and custom_name.
659
660 Function paint_theme_sample() applies the colors from the theme
661 to the tags in text widget highlight_sample and then invokes
662 set_color_sample(). Function set_highlight_target() sets the state
663 of the radiobuttons fg_on and bg_on based on the tag and it also
664 invokes set_color_sample().
665
666 Function set_color_sample() sets the background color for the frame
667 holding the color selector. This provides a larger visual of the
668 color for the current tag and plane (foreground/background).
669
670 Note: set_color_sample() is called from many places and is often
671 called more than once when a change is made. It is invoked when
672 foreground or background is selected (radiobuttons), from
673 paint_theme_sample() (theme is changed or load_cfg is called), and
674 from set_highlight_target() (target tag is changed or load_cfg called).
675
676 Button delete_custom invokes delete_custom() to delete
677 a custom theme from idleConf.userCfg['highlight'] and changes.
678 Button save_custom invokes save_as_new_theme() which calls
679 get_new_theme_name() and create_new() to save a custom theme
680 and its colors to idleConf.userCfg['highlight'].
681
682 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
683 if the current selected color for a tag is for the foreground or
684 background.
685
686 DynOptionMenu targetlist contains a readable description of the
687 tags applied to Python source within IDLE. Selecting one of the
688 tags from this list populates highlight_target, which has a callback
689 function set_highlight_target().
690
691 Text widget highlight_sample displays a block of text (which is
692 mock Python code) in which is embedded the defined tags and reflects
693 the color attributes of the current theme and changes for those tags.
694 Mouse button 1 allows for selection of a tag and updates
695 highlight_target with that tag value.
696
697 Note: The font in highlight_sample is set through the config in
698 the fonts tab.
699
700 In other words, a tag can be selected either from targetlist or
701 by clicking on the sample text within highlight_sample. The
702 plane (foreground/background) is selected via the radiobutton.
703 Together, these two (tag and plane) control what color is
704 shown in set_color_sample() for the current theme. Button set_color
705 invokes get_color() which displays a ColorChooser to change the
706 color for the selected tag/plane. If a new color is picked,
707 it will be saved to changes and the highlight_sample and
708 frame background will be updated.
709
710 Tk Variables:
711 color: Color of selected target.
712 builtin_name: Menu variable for built-in theme.
713 custom_name: Menu variable for custom theme.
714 fg_bg_toggle: Toggle for foreground/background color.
715 Note: this has no callback.
716 theme_source: Selector for built-in or custom theme.
717 highlight_target: Menu variable for the highlight tag target.
718
719 Instance Data Attributes:
720 theme_elements: Dictionary of tags for text highlighting.
721 The key is the display name and the value is a tuple of
722 (tag name, display sort order).
723
724 Methods [attachment]:
725 load_theme_cfg: Load current highlight colors.
726 get_color: Invoke colorchooser [button_set_color].
727 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
728 set_highlight_target: set fg_bg_toggle, set_color_sample().
729 set_color_sample: Set frame background to target.
730 on_new_color_set: Set new color and add option.
731 paint_theme_sample: Recolor sample.
732 get_new_theme_name: Get from popup.
733 create_new: Combine theme with changes and save.
734 save_as_new_theme: Save [button_save_custom].
735 set_theme_type: Command for [theme_source].
736 delete_custom: Activate default [button_delete_custom].
737 save_new: Save to userCfg['theme'] (is function).
738
739 Widgets of highlights page frame: (*) widgets bound to self
740 frame_custom: LabelFrame
741 (*)highlight_sample: Text
742 (*)frame_color_set: Frame
743 (*)button_set_color: Button
744 (*)targetlist: DynOptionMenu - highlight_target
745 frame_fg_bg_toggle: Frame
746 (*)fg_on: Radiobutton - fg_bg_toggle
747 (*)bg_on: Radiobutton - fg_bg_toggle
748 (*)button_save_custom: Button
749 frame_theme: LabelFrame
750 theme_type_title: Label
751 (*)builtin_theme_on: Radiobutton - theme_source
752 (*)custom_theme_on: Radiobutton - theme_source
753 (*)builtinlist: DynOptionMenu - builtin_name
754 (*)customlist: DynOptionMenu - custom_name
755 (*)button_delete_custom: Button
756 (*)theme_message: Label
757 """
758 self.theme_elements = {
759 'Normal Text': ('normal', '00'),
760 'Python Keywords': ('keyword', '01'),
761 'Python Definitions': ('definition', '02'),
762 'Python Builtins': ('builtin', '03'),
763 'Python Comments': ('comment', '04'),
764 'Python Strings': ('string', '05'),
765 'Selected Text': ('hilite', '06'),
766 'Found Text': ('hit', '07'),
767 'Cursor': ('cursor', '08'),
768 'Editor Breakpoint': ('break', '09'),
769 'Shell Normal Text': ('console', '10'),
770 'Shell Error Text': ('error', '11'),
771 'Shell Stdout Text': ('stdout', '12'),
772 'Shell Stderr Text': ('stderr', '13'),
773 }
774 self.builtin_name = tracers.add(
775 StringVar(self), self.var_changed_builtin_name)
776 self.custom_name = tracers.add(
777 StringVar(self), self.var_changed_custom_name)
778 self.fg_bg_toggle = BooleanVar(self)
779 self.color = tracers.add(
780 StringVar(self), self.var_changed_color)
781 self.theme_source = tracers.add(
782 BooleanVar(self), self.var_changed_theme_source)
783 self.highlight_target = tracers.add(
784 StringVar(self), self.var_changed_highlight_target)
785
786 # Create widgets:
787 # body frame and section frames.
788 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
789 text=' Custom Highlighting ')
790 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
791 text=' Highlighting Theme ')
792 # frame_custom.
793 text = self.highlight_sample = Text(
794 frame_custom, relief=SOLID, borderwidth=1,
795 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
796 takefocus=FALSE, highlightthickness=0, wrap=NONE)
797 text.bind('<Double-Button-1>', lambda e: 'break')
798 text.bind('<B1-Motion>', lambda e: 'break')
wohlganger58fc71c2017-09-10 16:19:47 -0500799 text_and_tags=(
800 ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400801 ('#you can click here', 'comment'), ('\n', 'normal'),
802 ('#to choose items', 'comment'), ('\n', 'normal'),
803 ('def', 'keyword'), (' ', 'normal'),
804 ('func', 'definition'), ('(param):\n ', 'normal'),
805 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
806 ("'string'", 'string'), ('\n var1 = ', 'normal'),
807 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
808 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
809 ('list', 'builtin'), ('(', 'normal'),
810 ('None', 'keyword'), (')\n', 'normal'),
811 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
812 (' error ', 'error'), (' ', 'normal'),
813 ('cursor |', 'cursor'), ('\n ', 'normal'),
814 ('shell', 'console'), (' ', 'normal'),
815 ('stdout', 'stdout'), (' ', 'normal'),
816 ('stderr', 'stderr'), ('\n\n', 'normal'))
817 for texttag in text_and_tags:
818 text.insert(END, texttag[0], texttag[1])
819 for element in self.theme_elements:
820 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400821 # event.widget.winfo_top_level().highlight_target.set(elem)
822 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400823 text.tag_bind(
824 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400825 text['state'] = 'disabled'
826 self.style.configure('frame_color_set.TFrame', borderwidth=1,
827 relief='solid')
828 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400829 frame_fg_bg_toggle = Frame(frame_custom)
830 self.button_set_color = Button(
831 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400832 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400833 self.targetlist = DynOptionMenu(
834 self.frame_color_set, self.highlight_target, None,
835 highlightthickness=0) #, command=self.set_highlight_targetBinding
836 self.fg_on = Radiobutton(
837 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
838 text='Foreground', command=self.set_color_sample_binding)
839 self.bg_on = Radiobutton(
840 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
841 text='Background', command=self.set_color_sample_binding)
842 self.fg_bg_toggle.set(1)
843 self.button_save_custom = Button(
844 frame_custom, text='Save as New Custom Theme',
845 command=self.save_as_new_theme)
846 # frame_theme.
847 theme_type_title = Label(frame_theme, text='Select : ')
848 self.builtin_theme_on = Radiobutton(
849 frame_theme, variable=self.theme_source, value=1,
850 command=self.set_theme_type, text='a Built-in Theme')
851 self.custom_theme_on = Radiobutton(
852 frame_theme, variable=self.theme_source, value=0,
853 command=self.set_theme_type, text='a Custom Theme')
854 self.builtinlist = DynOptionMenu(
855 frame_theme, self.builtin_name, None, command=None)
856 self.customlist = DynOptionMenu(
857 frame_theme, self.custom_name, None, command=None)
858 self.button_delete_custom = Button(
859 frame_theme, text='Delete Custom Theme',
860 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400861 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400862 # Pack widgets:
863 # body.
864 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500865 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400866 # frame_custom.
867 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
868 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
869 self.highlight_sample.pack(
870 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
871 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
872 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
873 self.fg_on.pack(side=LEFT, anchor=E)
874 self.bg_on.pack(side=RIGHT, anchor=W)
875 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
876 # frame_theme.
877 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
878 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
879 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
880 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
881 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
882 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
883 self.theme_message.pack(side=TOP, fill=X, pady=5)
884
885 def load_theme_cfg(self):
886 """Load current configuration settings for the theme options.
887
888 Based on the theme_source toggle, the theme is set as
889 either builtin or custom and the initial widget values
890 reflect the current settings from idleConf.
891
892 Attributes updated:
893 theme_source: Set from idleConf.
894 builtinlist: List of default themes from idleConf.
895 customlist: List of custom themes from idleConf.
896 custom_theme_on: Disabled if there are no custom themes.
897 custom_theme: Message with additional information.
898 targetlist: Create menu from self.theme_elements.
899
900 Methods:
901 set_theme_type
902 paint_theme_sample
903 set_highlight_target
904 """
905 # Set current theme type radiobutton.
906 self.theme_source.set(idleConf.GetOption(
907 'main', 'Theme', 'default', type='bool', default=1))
908 # Set current theme.
909 current_option = idleConf.CurrentTheme()
910 # Load available theme option menus.
911 if self.theme_source.get(): # Default theme selected.
912 item_list = idleConf.GetSectionList('default', 'highlight')
913 item_list.sort()
914 self.builtinlist.SetMenu(item_list, current_option)
915 item_list = idleConf.GetSectionList('user', 'highlight')
916 item_list.sort()
917 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400918 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400919 self.custom_name.set('- no custom themes -')
920 else:
921 self.customlist.SetMenu(item_list, item_list[0])
922 else: # User theme selected.
923 item_list = idleConf.GetSectionList('user', 'highlight')
924 item_list.sort()
925 self.customlist.SetMenu(item_list, current_option)
926 item_list = idleConf.GetSectionList('default', 'highlight')
927 item_list.sort()
928 self.builtinlist.SetMenu(item_list, item_list[0])
929 self.set_theme_type()
930 # Load theme element option menu.
931 theme_names = list(self.theme_elements.keys())
932 theme_names.sort(key=lambda x: self.theme_elements[x][1])
933 self.targetlist.SetMenu(theme_names, theme_names[0])
934 self.paint_theme_sample()
935 self.set_highlight_target()
936
937 def var_changed_builtin_name(self, *params):
938 """Process new builtin theme selection.
939
940 Add the changed theme's name to the changed_items and recreate
941 the sample with the values from the selected theme.
942 """
943 old_themes = ('IDLE Classic', 'IDLE New')
944 value = self.builtin_name.get()
945 if value not in old_themes:
946 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
947 changes.add_option('main', 'Theme', 'name', old_themes[0])
948 changes.add_option('main', 'Theme', 'name2', value)
949 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400950 else:
951 changes.add_option('main', 'Theme', 'name', value)
952 changes.add_option('main', 'Theme', 'name2', '')
953 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400954 self.paint_theme_sample()
955
956 def var_changed_custom_name(self, *params):
957 """Process new custom theme selection.
958
959 If a new custom theme is selected, add the name to the
960 changed_items and apply the theme to the sample.
961 """
962 value = self.custom_name.get()
963 if value != '- no custom themes -':
964 changes.add_option('main', 'Theme', 'name', value)
965 self.paint_theme_sample()
966
967 def var_changed_theme_source(self, *params):
968 """Process toggle between builtin and custom theme.
969
970 Update the default toggle value and apply the newly
971 selected theme type.
972 """
973 value = self.theme_source.get()
974 changes.add_option('main', 'Theme', 'default', value)
975 if value:
976 self.var_changed_builtin_name()
977 else:
978 self.var_changed_custom_name()
979
980 def var_changed_color(self, *params):
981 "Process change to color choice."
982 self.on_new_color_set()
983
984 def var_changed_highlight_target(self, *params):
985 "Process selection of new target tag for highlighting."
986 self.set_highlight_target()
987
988 def set_theme_type(self):
989 """Set available screen options based on builtin or custom theme.
990
991 Attributes accessed:
992 theme_source
993
994 Attributes updated:
995 builtinlist
996 customlist
997 button_delete_custom
998 custom_theme_on
999
1000 Called from:
1001 handler for builtin_theme_on and custom_theme_on
1002 delete_custom
1003 create_new
1004 load_theme_cfg
1005 """
1006 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001007 self.builtinlist['state'] = 'normal'
1008 self.customlist['state'] = 'disabled'
1009 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001010 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001011 self.builtinlist['state'] = 'disabled'
1012 self.custom_theme_on.state(('!disabled',))
1013 self.customlist['state'] = 'normal'
1014 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001015
1016 def get_color(self):
1017 """Handle button to select a new color for the target tag.
1018
1019 If a new color is selected while using a builtin theme, a
1020 name must be supplied to create a custom theme.
1021
1022 Attributes accessed:
1023 highlight_target
1024 frame_color_set
1025 theme_source
1026
1027 Attributes updated:
1028 color
1029
1030 Methods:
1031 get_new_theme_name
1032 create_new
1033 """
1034 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001035 prev_color = self.style.lookup(self.frame_color_set['style'],
1036 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001037 rgbTuplet, color_string = tkColorChooser.askcolor(
1038 parent=self, title='Pick new color for : '+target,
1039 initialcolor=prev_color)
1040 if color_string and (color_string != prev_color):
1041 # User didn't cancel and they chose a new color.
1042 if self.theme_source.get(): # Current theme is a built-in.
1043 message = ('Your changes will be saved as a new Custom Theme. '
1044 'Enter a name for your new Custom Theme below.')
1045 new_theme = self.get_new_theme_name(message)
1046 if not new_theme: # User cancelled custom theme creation.
1047 return
1048 else: # Create new custom theme based on previously active theme.
1049 self.create_new(new_theme)
1050 self.color.set(color_string)
1051 else: # Current theme is user defined.
1052 self.color.set(color_string)
1053
1054 def on_new_color_set(self):
1055 "Display sample of new color selection on the dialog."
1056 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001057 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001058 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1059 sample_element = self.theme_elements[self.highlight_target.get()][0]
1060 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1061 theme = self.custom_name.get()
1062 theme_element = sample_element + '-' + plane
1063 changes.add_option('highlight', theme, theme_element, new_color)
1064
1065 def get_new_theme_name(self, message):
1066 "Return name of new theme from query popup."
1067 used_names = (idleConf.GetSectionList('user', 'highlight') +
1068 idleConf.GetSectionList('default', 'highlight'))
1069 new_theme = SectionName(
1070 self, 'New Custom Theme', message, used_names).result
1071 return new_theme
1072
1073 def save_as_new_theme(self):
1074 """Prompt for new theme name and create the theme.
1075
1076 Methods:
1077 get_new_theme_name
1078 create_new
1079 """
1080 new_theme_name = self.get_new_theme_name('New Theme Name:')
1081 if new_theme_name:
1082 self.create_new(new_theme_name)
1083
1084 def create_new(self, new_theme_name):
1085 """Create a new custom theme with the given name.
1086
1087 Create the new theme based on the previously active theme
1088 with the current changes applied. Once it is saved, then
1089 activate the new theme.
1090
1091 Attributes accessed:
1092 builtin_name
1093 custom_name
1094
1095 Attributes updated:
1096 customlist
1097 theme_source
1098
1099 Method:
1100 save_new
1101 set_theme_type
1102 """
1103 if self.theme_source.get():
1104 theme_type = 'default'
1105 theme_name = self.builtin_name.get()
1106 else:
1107 theme_type = 'user'
1108 theme_name = self.custom_name.get()
1109 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1110 # Apply any of the old theme's unsaved changes to the new theme.
1111 if theme_name in changes['highlight']:
1112 theme_changes = changes['highlight'][theme_name]
1113 for element in theme_changes:
1114 new_theme[element] = theme_changes[element]
1115 # Save the new theme.
1116 self.save_new(new_theme_name, new_theme)
1117 # Change GUI over to the new theme.
1118 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1119 custom_theme_list.sort()
1120 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1121 self.theme_source.set(0)
1122 self.set_theme_type()
1123
1124 def set_highlight_target(self):
1125 """Set fg/bg toggle and color based on highlight tag target.
1126
1127 Instance variables accessed:
1128 highlight_target
1129
1130 Attributes updated:
1131 fg_on
1132 bg_on
1133 fg_bg_toggle
1134
1135 Methods:
1136 set_color_sample
1137
1138 Called from:
1139 var_changed_highlight_target
1140 load_theme_cfg
1141 """
1142 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001143 self.fg_on.state(('disabled',))
1144 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001145 self.fg_bg_toggle.set(1)
1146 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001147 self.fg_on.state(('!disabled',))
1148 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001149 self.fg_bg_toggle.set(1)
1150 self.set_color_sample()
1151
1152 def set_color_sample_binding(self, *args):
1153 """Change color sample based on foreground/background toggle.
1154
1155 Methods:
1156 set_color_sample
1157 """
1158 self.set_color_sample()
1159
1160 def set_color_sample(self):
1161 """Set the color of the frame background to reflect the selected target.
1162
1163 Instance variables accessed:
1164 theme_elements
1165 highlight_target
1166 fg_bg_toggle
1167 highlight_sample
1168
1169 Attributes updated:
1170 frame_color_set
1171 """
1172 # Set the color sample area.
1173 tag = self.theme_elements[self.highlight_target.get()][0]
1174 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1175 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001176 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001177
1178 def paint_theme_sample(self):
1179 """Apply the theme colors to each element tag in the sample text.
1180
1181 Instance attributes accessed:
1182 theme_elements
1183 theme_source
1184 builtin_name
1185 custom_name
1186
1187 Attributes updated:
1188 highlight_sample: Set the tag elements to the theme.
1189
1190 Methods:
1191 set_color_sample
1192
1193 Called from:
1194 var_changed_builtin_name
1195 var_changed_custom_name
1196 load_theme_cfg
1197 """
1198 if self.theme_source.get(): # Default theme
1199 theme = self.builtin_name.get()
1200 else: # User theme
1201 theme = self.custom_name.get()
1202 for element_title in self.theme_elements:
1203 element = self.theme_elements[element_title][0]
1204 colors = idleConf.GetHighlight(theme, element)
1205 if element == 'cursor': # Cursor sample needs special painting.
1206 colors['background'] = idleConf.GetHighlight(
1207 theme, 'normal', fgBg='bg')
1208 # Handle any unsaved changes to this theme.
1209 if theme in changes['highlight']:
1210 theme_dict = changes['highlight'][theme]
1211 if element + '-foreground' in theme_dict:
1212 colors['foreground'] = theme_dict[element + '-foreground']
1213 if element + '-background' in theme_dict:
1214 colors['background'] = theme_dict[element + '-background']
1215 self.highlight_sample.tag_config(element, **colors)
1216 self.set_color_sample()
1217
1218 def save_new(self, theme_name, theme):
1219 """Save a newly created theme to idleConf.
1220
1221 theme_name - string, the name of the new theme
1222 theme - dictionary containing the new theme
1223 """
1224 if not idleConf.userCfg['highlight'].has_section(theme_name):
1225 idleConf.userCfg['highlight'].add_section(theme_name)
1226 for element in theme:
1227 value = theme[element]
1228 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1229
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001230 def askyesno(self, *args, **kwargs):
1231 # Make testing easier. Could change implementation.
1232 messagebox.askyesno(*args, **kwargs)
1233
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001234 def delete_custom(self):
1235 """Handle event to delete custom theme.
1236
1237 The current theme is deactivated and the default theme is
1238 activated. The custom theme is permanently removed from
1239 the config file.
1240
1241 Attributes accessed:
1242 custom_name
1243
1244 Attributes updated:
1245 custom_theme_on
1246 customlist
1247 theme_source
1248 builtin_name
1249
1250 Methods:
1251 deactivate_current_config
1252 save_all_changed_extensions
1253 activate_config_changes
1254 set_theme_type
1255 """
1256 theme_name = self.custom_name.get()
1257 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001258 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001259 'Delete Theme', delmsg % theme_name, parent=self):
1260 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001261 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001262 # Remove theme from changes, config, and file.
1263 changes.delete_section('highlight', theme_name)
1264 # Reload user theme list.
1265 item_list = idleConf.GetSectionList('user', 'highlight')
1266 item_list.sort()
1267 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001268 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001269 self.customlist.SetMenu(item_list, '- no custom themes -')
1270 else:
1271 self.customlist.SetMenu(item_list, item_list[0])
1272 # Revert to default theme.
1273 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1274 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1275 # User can't back out of these changes, they must be applied now.
1276 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001277 self.cd.save_all_changed_extensions()
1278 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001279 self.set_theme_type()
1280
1281
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001282class KeysPage(Frame):
1283
1284 def __init__(self, master):
1285 super().__init__(master)
1286 self.cd = master.master
1287 self.create_page_keys()
1288 self.load_key_cfg()
1289
1290 def create_page_keys(self):
1291 """Return frame of widgets for Keys tab.
1292
1293 Enable users to provisionally change both individual and sets of
1294 keybindings (shortcut keys). Except for features implemented as
1295 extensions, keybindings are stored in complete sets called
1296 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1297 as far as the dialog is concerned. Any keyset can be used as the
1298 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1299
1300 Function load_key_cfg() initializes tk variables and keyset
1301 lists and calls load_keys_list for the current keyset.
1302 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1303 keyset_source, which controls if the current set of keybindings
1304 are from a builtin or custom keyset. DynOptionMenus builtinlist
1305 and customlist contain lists of the builtin and custom keysets,
1306 respectively, and the current item from each list is stored in
1307 vars builtin_name and custom_name.
1308
1309 Button delete_custom_keys invokes delete_custom_keys() to delete
1310 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1311 save_custom_keys invokes save_as_new_key_set() which calls
1312 get_new_keys_name() and create_new_key_set() to save a custom keyset
1313 and its keybindings to idleConf.userCfg['keys'].
1314
1315 Listbox bindingslist contains all of the keybindings for the
1316 selected keyset. The keybindings are loaded in load_keys_list()
1317 and are pairs of (event, [keys]) where keys can be a list
1318 of one or more key combinations to bind to the same event.
1319 Mouse button 1 click invokes on_bindingslist_select(), which
1320 allows button_new_keys to be clicked.
1321
1322 So, an item is selected in listbindings, which activates
1323 button_new_keys, and clicking button_new_keys calls function
1324 get_new_keys(). Function get_new_keys() gets the key mappings from the
1325 current keyset for the binding event item that was selected. The
1326 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001327 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001328 to be entered for that binding event. If the keys aren't
1329 changed, nothing happens. If the keys are changed and the keyset
1330 is a builtin, function get_new_keys_name() will be called
1331 for input of a custom keyset name. If no name is given, then the
1332 change to the keybinding will abort and no updates will be made. If
1333 a custom name is entered in the prompt or if the current keyset was
1334 already custom (and thus didn't require a prompt), then
1335 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1336 with the change to the event binding. The item listing in bindingslist
1337 is updated with the new keys. Var keybinding is also set which invokes
1338 the callback function, var_changed_keybinding, to add the change to
1339 the 'keys' or 'extensions' changes tracker based on the binding type.
1340
1341 Tk Variables:
1342 keybinding: Action/key bindings.
1343
1344 Methods:
1345 load_keys_list: Reload active set.
1346 create_new_key_set: Combine active keyset and changes.
1347 set_keys_type: Command for keyset_source.
1348 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1349 deactivate_current_config: Remove keys bindings in editors.
1350
1351 Widgets for KeysPage(frame): (*) widgets bound to self
1352 frame_key_sets: LabelFrame
1353 frames[0]: Frame
1354 (*)builtin_keyset_on: Radiobutton - var keyset_source
1355 (*)custom_keyset_on: Radiobutton - var keyset_source
1356 (*)builtinlist: DynOptionMenu - var builtin_name,
1357 func keybinding_selected
1358 (*)customlist: DynOptionMenu - var custom_name,
1359 func keybinding_selected
1360 (*)keys_message: Label
1361 frames[1]: Frame
1362 (*)button_delete_custom_keys: Button - delete_custom_keys
1363 (*)button_save_custom_keys: Button - save_as_new_key_set
1364 frame_custom: LabelFrame
1365 frame_target: Frame
1366 target_title: Label
1367 scroll_target_y: Scrollbar
1368 scroll_target_x: Scrollbar
1369 (*)bindingslist: ListBox - on_bindingslist_select
1370 (*)button_new_keys: Button - get_new_keys & ..._name
1371 """
1372 self.builtin_name = tracers.add(
1373 StringVar(self), self.var_changed_builtin_name)
1374 self.custom_name = tracers.add(
1375 StringVar(self), self.var_changed_custom_name)
1376 self.keyset_source = tracers.add(
1377 BooleanVar(self), self.var_changed_keyset_source)
1378 self.keybinding = tracers.add(
1379 StringVar(self), self.var_changed_keybinding)
1380
1381 # Create widgets:
1382 # body and section frames.
1383 frame_custom = LabelFrame(
1384 self, borderwidth=2, relief=GROOVE,
1385 text=' Custom Key Bindings ')
1386 frame_key_sets = LabelFrame(
1387 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1388 # frame_custom.
1389 frame_target = Frame(frame_custom)
1390 target_title = Label(frame_target, text='Action - Key(s)')
1391 scroll_target_y = Scrollbar(frame_target)
1392 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1393 self.bindingslist = Listbox(
1394 frame_target, takefocus=FALSE, exportselection=FALSE)
1395 self.bindingslist.bind('<ButtonRelease-1>',
1396 self.on_bindingslist_select)
1397 scroll_target_y['command'] = self.bindingslist.yview
1398 scroll_target_x['command'] = self.bindingslist.xview
1399 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1400 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1401 self.button_new_keys = Button(
1402 frame_custom, text='Get New Keys for Selection',
1403 command=self.get_new_keys, state=DISABLED)
1404 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001405 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001406 for i in range(2)]
1407 self.builtin_keyset_on = Radiobutton(
1408 frames[0], variable=self.keyset_source, value=1,
1409 command=self.set_keys_type, text='Use a Built-in Key Set')
1410 self.custom_keyset_on = Radiobutton(
1411 frames[0], variable=self.keyset_source, value=0,
1412 command=self.set_keys_type, text='Use a Custom Key Set')
1413 self.builtinlist = DynOptionMenu(
1414 frames[0], self.builtin_name, None, command=None)
1415 self.customlist = DynOptionMenu(
1416 frames[0], self.custom_name, None, command=None)
1417 self.button_delete_custom_keys = Button(
1418 frames[1], text='Delete Custom Key Set',
1419 command=self.delete_custom_keys)
1420 self.button_save_custom_keys = Button(
1421 frames[1], text='Save as New Custom Key Set',
1422 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001423 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001424
1425 # Pack widgets:
1426 # body.
1427 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1428 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1429 # frame_custom.
1430 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1431 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1432 # frame_target.
1433 frame_target.columnconfigure(0, weight=1)
1434 frame_target.rowconfigure(1, weight=1)
1435 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1436 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1437 scroll_target_y.grid(row=1, column=1, sticky=NS)
1438 scroll_target_x.grid(row=2, column=0, sticky=EW)
1439 # frame_key_sets.
1440 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1441 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1442 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1443 self.customlist.grid(row=1, column=1, sticky=NSEW)
1444 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1445 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1446 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1447 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1448 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1449
1450 def load_key_cfg(self):
1451 "Load current configuration settings for the keybinding options."
1452 # Set current keys type radiobutton.
1453 self.keyset_source.set(idleConf.GetOption(
1454 'main', 'Keys', 'default', type='bool', default=1))
1455 # Set current keys.
1456 current_option = idleConf.CurrentKeys()
1457 # Load available keyset option menus.
1458 if self.keyset_source.get(): # Default theme selected.
1459 item_list = idleConf.GetSectionList('default', 'keys')
1460 item_list.sort()
1461 self.builtinlist.SetMenu(item_list, current_option)
1462 item_list = idleConf.GetSectionList('user', 'keys')
1463 item_list.sort()
1464 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001465 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001466 self.custom_name.set('- no custom keys -')
1467 else:
1468 self.customlist.SetMenu(item_list, item_list[0])
1469 else: # User key set selected.
1470 item_list = idleConf.GetSectionList('user', 'keys')
1471 item_list.sort()
1472 self.customlist.SetMenu(item_list, current_option)
1473 item_list = idleConf.GetSectionList('default', 'keys')
1474 item_list.sort()
1475 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1476 self.set_keys_type()
1477 # Load keyset element list.
1478 keyset_name = idleConf.CurrentKeys()
1479 self.load_keys_list(keyset_name)
1480
1481 def var_changed_builtin_name(self, *params):
1482 "Process selection of builtin key set."
1483 old_keys = (
1484 'IDLE Classic Windows',
1485 'IDLE Classic Unix',
1486 'IDLE Classic Mac',
1487 'IDLE Classic OSX',
1488 )
1489 value = self.builtin_name.get()
1490 if value not in old_keys:
1491 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1492 changes.add_option('main', 'Keys', 'name', old_keys[0])
1493 changes.add_option('main', 'Keys', 'name2', value)
1494 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001495 else:
1496 changes.add_option('main', 'Keys', 'name', value)
1497 changes.add_option('main', 'Keys', 'name2', '')
1498 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001499 self.load_keys_list(value)
1500
1501 def var_changed_custom_name(self, *params):
1502 "Process selection of custom key set."
1503 value = self.custom_name.get()
1504 if value != '- no custom keys -':
1505 changes.add_option('main', 'Keys', 'name', value)
1506 self.load_keys_list(value)
1507
1508 def var_changed_keyset_source(self, *params):
1509 "Process toggle between builtin key set and custom key set."
1510 value = self.keyset_source.get()
1511 changes.add_option('main', 'Keys', 'default', value)
1512 if value:
1513 self.var_changed_builtin_name()
1514 else:
1515 self.var_changed_custom_name()
1516
1517 def var_changed_keybinding(self, *params):
1518 "Store change to a keybinding."
1519 value = self.keybinding.get()
1520 key_set = self.custom_name.get()
1521 event = self.bindingslist.get(ANCHOR).split()[0]
1522 if idleConf.IsCoreBinding(event):
1523 changes.add_option('keys', key_set, event, value)
1524 else: # Event is an extension binding.
1525 ext_name = idleConf.GetExtnNameForEvent(event)
1526 ext_keybind_section = ext_name + '_cfgBindings'
1527 changes.add_option('extensions', ext_keybind_section, event, value)
1528
1529 def set_keys_type(self):
1530 "Set available screen options based on builtin or custom key set."
1531 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001532 self.builtinlist['state'] = 'normal'
1533 self.customlist['state'] = 'disabled'
1534 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001535 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001536 self.builtinlist['state'] = 'disabled'
1537 self.custom_keyset_on.state(('!disabled',))
1538 self.customlist['state'] = 'normal'
1539 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001540
1541 def get_new_keys(self):
1542 """Handle event to change key binding for selected line.
1543
1544 A selection of a key/binding in the list of current
1545 bindings pops up a dialog to enter a new binding. If
1546 the current key set is builtin and a binding has
1547 changed, then a name for a custom key set needs to be
1548 entered for the change to be applied.
1549 """
1550 list_index = self.bindingslist.index(ANCHOR)
1551 binding = self.bindingslist.get(list_index)
1552 bind_name = binding.split()[0]
1553 if self.keyset_source.get():
1554 current_key_set_name = self.builtin_name.get()
1555 else:
1556 current_key_set_name = self.custom_name.get()
1557 current_bindings = idleConf.GetCurrentKeySet()
1558 if current_key_set_name in changes['keys']: # unsaved changes
1559 key_set_changes = changes['keys'][current_key_set_name]
1560 for event in key_set_changes:
1561 current_bindings[event] = key_set_changes[event].split()
1562 current_key_sequences = list(current_bindings.values())
1563 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1564 current_key_sequences).result
1565 if new_keys:
1566 if self.keyset_source.get(): # Current key set is a built-in.
1567 message = ('Your changes will be saved as a new Custom Key Set.'
1568 ' Enter a name for your new Custom Key Set below.')
1569 new_keyset = self.get_new_keys_name(message)
1570 if not new_keyset: # User cancelled custom key set creation.
1571 self.bindingslist.select_set(list_index)
1572 self.bindingslist.select_anchor(list_index)
1573 return
1574 else: # Create new custom key set based on previously active key set.
1575 self.create_new_key_set(new_keyset)
1576 self.bindingslist.delete(list_index)
1577 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1578 self.bindingslist.select_set(list_index)
1579 self.bindingslist.select_anchor(list_index)
1580 self.keybinding.set(new_keys)
1581 else:
1582 self.bindingslist.select_set(list_index)
1583 self.bindingslist.select_anchor(list_index)
1584
1585 def get_new_keys_name(self, message):
1586 "Return new key set name from query popup."
1587 used_names = (idleConf.GetSectionList('user', 'keys') +
1588 idleConf.GetSectionList('default', 'keys'))
1589 new_keyset = SectionName(
1590 self, 'New Custom Key Set', message, used_names).result
1591 return new_keyset
1592
1593 def save_as_new_key_set(self):
1594 "Prompt for name of new key set and save changes using that name."
1595 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1596 if new_keys_name:
1597 self.create_new_key_set(new_keys_name)
1598
1599 def on_bindingslist_select(self, event):
1600 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001601 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001602
1603 def create_new_key_set(self, new_key_set_name):
1604 """Create a new custom key set with the given name.
1605
1606 Copy the bindings/keys from the previously active keyset
1607 to the new keyset and activate the new custom keyset.
1608 """
1609 if self.keyset_source.get():
1610 prev_key_set_name = self.builtin_name.get()
1611 else:
1612 prev_key_set_name = self.custom_name.get()
1613 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1614 new_keys = {}
1615 for event in prev_keys: # Add key set to changed items.
1616 event_name = event[2:-2] # Trim off the angle brackets.
1617 binding = ' '.join(prev_keys[event])
1618 new_keys[event_name] = binding
1619 # Handle any unsaved changes to prev key set.
1620 if prev_key_set_name in changes['keys']:
1621 key_set_changes = changes['keys'][prev_key_set_name]
1622 for event in key_set_changes:
1623 new_keys[event] = key_set_changes[event]
1624 # Save the new key set.
1625 self.save_new_key_set(new_key_set_name, new_keys)
1626 # Change GUI over to the new key set.
1627 custom_key_list = idleConf.GetSectionList('user', 'keys')
1628 custom_key_list.sort()
1629 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1630 self.keyset_source.set(0)
1631 self.set_keys_type()
1632
1633 def load_keys_list(self, keyset_name):
1634 """Reload the list of action/key binding pairs for the active key set.
1635
1636 An action/key binding can be selected to change the key binding.
1637 """
1638 reselect = False
1639 if self.bindingslist.curselection():
1640 reselect = True
1641 list_index = self.bindingslist.index(ANCHOR)
1642 keyset = idleConf.GetKeySet(keyset_name)
1643 bind_names = list(keyset.keys())
1644 bind_names.sort()
1645 self.bindingslist.delete(0, END)
1646 for bind_name in bind_names:
1647 key = ' '.join(keyset[bind_name])
1648 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1649 if keyset_name in changes['keys']:
1650 # Handle any unsaved changes to this key set.
1651 if bind_name in changes['keys'][keyset_name]:
1652 key = changes['keys'][keyset_name][bind_name]
1653 self.bindingslist.insert(END, bind_name+' - '+key)
1654 if reselect:
1655 self.bindingslist.see(list_index)
1656 self.bindingslist.select_set(list_index)
1657 self.bindingslist.select_anchor(list_index)
1658
1659 @staticmethod
1660 def save_new_key_set(keyset_name, keyset):
1661 """Save a newly created core key set.
1662
1663 Add keyset to idleConf.userCfg['keys'], not to disk.
1664 If the keyset doesn't exist, it is created. The
1665 binding/keys are taken from the keyset argument.
1666
1667 keyset_name - string, the name of the new key set
1668 keyset - dictionary containing the new keybindings
1669 """
1670 if not idleConf.userCfg['keys'].has_section(keyset_name):
1671 idleConf.userCfg['keys'].add_section(keyset_name)
1672 for event in keyset:
1673 value = keyset[event]
1674 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1675
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001676 def askyesno(self, *args, **kwargs):
1677 # Make testing easier. Could change implementation.
1678 messagebox.askyesno(*args, **kwargs)
1679
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001680 def delete_custom_keys(self):
1681 """Handle event to delete a custom key set.
1682
1683 Applying the delete deactivates the current configuration and
1684 reverts to the default. The custom key set is permanently
1685 deleted from the config file.
1686 """
1687 keyset_name = self.custom_name.get()
1688 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001689 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001690 'Delete Key Set', delmsg % keyset_name, parent=self):
1691 return
1692 self.cd.deactivate_current_config()
1693 # Remove key set from changes, config, and file.
1694 changes.delete_section('keys', keyset_name)
1695 # Reload user key set list.
1696 item_list = idleConf.GetSectionList('user', 'keys')
1697 item_list.sort()
1698 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001699 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001700 self.customlist.SetMenu(item_list, '- no custom keys -')
1701 else:
1702 self.customlist.SetMenu(item_list, item_list[0])
1703 # Revert to default key set.
1704 self.keyset_source.set(idleConf.defaultCfg['main']
1705 .Get('Keys', 'default'))
1706 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1707 or idleConf.default_keys())
1708 # User can't back out of these changes, they must be applied now.
1709 changes.save_all()
1710 self.cd.save_all_changed_extensions()
1711 self.cd.activate_config_changes()
1712 self.set_keys_type()
1713
1714
csabellae8eb17b2017-07-30 18:39:17 -04001715class GenPage(Frame):
1716
csabella6f446be2017-08-01 00:24:07 -04001717 def __init__(self, master):
1718 super().__init__(master)
csabellae8eb17b2017-07-30 18:39:17 -04001719 self.create_page_general()
1720 self.load_general_cfg()
1721
1722 def create_page_general(self):
1723 """Return frame of widgets for General tab.
1724
1725 Enable users to provisionally change general options. Function
1726 load_general_cfg intializes tk variables and helplist using
1727 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1728 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1729 set var autosave. Entry boxes win_width_int and win_height_int
1730 set var win_width and win_height. Setting var_name invokes the
1731 default callback that adds option to changes.
1732
1733 Helplist: load_general_cfg loads list user_helplist with
1734 name, position pairs and copies names to listbox helplist.
1735 Clicking a name invokes help_source selected. Clicking
1736 button_helplist_name invokes helplist_item_name, which also
1737 changes user_helplist. These functions all call
1738 set_add_delete_state. All but load call update_help_changes to
1739 rewrite changes['main']['HelpFiles'].
1740
Cheryl Sabella2f896462017-08-14 21:21:43 -04001741 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001742 frame_window: LabelFrame
1743 frame_run: Frame
1744 startup_title: Label
1745 (*)startup_editor_on: Radiobutton - startup_edit
1746 (*)startup_shell_on: Radiobutton - startup_edit
1747 frame_win_size: Frame
1748 win_size_title: Label
1749 win_width_title: Label
1750 (*)win_width_int: Entry - win_width
1751 win_height_title: Label
1752 (*)win_height_int: Entry - win_height
1753 frame_editor: LabelFrame
1754 frame_save: Frame
1755 run_save_title: Label
1756 (*)save_ask_on: Radiobutton - autosave
1757 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella2f896462017-08-14 21:21:43 -04001758 frame_help: LabelFrame
1759 frame_helplist: Frame
1760 frame_helplist_buttons: Frame
1761 (*)button_helplist_edit
1762 (*)button_helplist_add
1763 (*)button_helplist_remove
1764 (*)helplist: ListBox
1765 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001766 """
wohlganger58fc71c2017-09-10 16:19:47 -05001767 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001768 self.startup_edit = tracers.add(
1769 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001770 self.win_width = tracers.add(
1771 StringVar(self), ('main', 'EditorWindow', 'width'))
1772 self.win_height = tracers.add(
1773 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001774 self.autocomplete_wait = tracers.add(
1775 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1776 self.paren_style = tracers.add(
1777 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1778 self.flash_delay = tracers.add(
1779 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1780 self.paren_bell = tracers.add(
1781 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001782
wohlganger58fc71c2017-09-10 16:19:47 -05001783 self.autosave = tracers.add(
1784 IntVar(self), ('main', 'General', 'autosave'))
1785 self.format_width = tracers.add(
1786 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
1787 self.context_lines = tracers.add(
1788 StringVar(self), ('extensions', 'CodeContext', 'numlines'))
1789
1790 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001791 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001792 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1793 text=' Window Preferences')
1794 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1795 text=' Editor Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001796 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1797 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001798 # Frame_window.
1799 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001800 startup_title = Label(frame_run, text='At Startup')
1801 self.startup_editor_on = Radiobutton(
1802 frame_run, variable=self.startup_edit, value=1,
1803 text="Open Edit Window")
1804 self.startup_shell_on = Radiobutton(
1805 frame_run, variable=self.startup_edit, value=0,
1806 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001807
wohlganger58fc71c2017-09-10 16:19:47 -05001808 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001809 win_size_title = Label(
1810 frame_win_size, text='Initial Window Size (in characters)')
1811 win_width_title = Label(frame_win_size, text='Width')
1812 self.win_width_int = Entry(
1813 frame_win_size, textvariable=self.win_width, width=3)
1814 win_height_title = Label(frame_win_size, text='Height')
1815 self.win_height_int = Entry(
1816 frame_win_size, textvariable=self.win_height, width=3)
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001817
wohlganger58fc71c2017-09-10 16:19:47 -05001818 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1819 auto_wait_title = Label(frame_autocomplete,
1820 text='Completions Popup Wait (milliseconds)')
1821 self.auto_wait_int = Entry(frame_autocomplete, width=6,
1822 textvariable=self.autocomplete_wait)
1823
1824 frame_paren1 = Frame(frame_window, borderwidth=0)
1825 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1826 self.paren_style_type = OptionMenu(
1827 frame_paren1, self.paren_style, 'expression',
1828 "opener","parens","expression")
1829 frame_paren2 = Frame(frame_window, borderwidth=0)
1830 paren_time_title = Label(
1831 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1832 '(0 is until next input)')
1833 self.paren_flash_time = Entry(
1834 frame_paren2, textvariable=self.flash_delay, width=6)
1835 self.bell_on = Checkbutton(
1836 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1837
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001838 # Frame_editor.
1839 frame_save = Frame(frame_editor, borderwidth=0)
1840 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1841 self.save_ask_on = Radiobutton(
1842 frame_save, variable=self.autosave, value=0,
1843 text="Prompt to Save")
1844 self.save_auto_on = Radiobutton(
1845 frame_save, variable=self.autosave, value=1,
1846 text='No Prompt')
1847
wohlganger58fc71c2017-09-10 16:19:47 -05001848 frame_format = Frame(frame_editor, borderwidth=0)
1849 format_width_title = Label(frame_format,
1850 text='Format Paragraph Max Width')
1851 self.format_width_int = Entry(
1852 frame_format, textvariable=self.format_width, width=4)
1853
1854 frame_context = Frame(frame_editor, borderwidth=0)
1855 context_title = Label(frame_context, text='Context Lines :')
1856 self.context_int = Entry(
1857 frame_context, textvariable=self.context_lines, width=3)
1858
1859
csabellae8eb17b2017-07-30 18:39:17 -04001860 # frame_help.
1861 frame_helplist = Frame(frame_help)
1862 frame_helplist_buttons = Frame(frame_helplist)
1863 self.helplist = Listbox(
1864 frame_helplist, height=5, takefocus=True,
1865 exportselection=FALSE)
1866 scroll_helplist = Scrollbar(frame_helplist)
1867 scroll_helplist['command'] = self.helplist.yview
1868 self.helplist['yscrollcommand'] = scroll_helplist.set
1869 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1870 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001871 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001872 width=8, command=self.helplist_item_edit)
1873 self.button_helplist_add = Button(
1874 frame_helplist_buttons, text='Add',
1875 width=8, command=self.helplist_item_add)
1876 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001877 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001878 width=8, command=self.helplist_item_remove)
1879
1880 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001881 # Body.
1882 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1883 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04001884 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1885 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001886 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001887 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1888 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1889 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04001890 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001891 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001892 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1893 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1894 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1895 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1896 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001897 # frame_autocomplete.
1898 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
1899 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1900 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
1901 # frame_paren.
1902 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
1903 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1904 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
1905 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
1906 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
1907 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1908 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
1909
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001910 # frame_save.
1911 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
1912 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1913 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1914 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001915 # frame_format.
1916 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
1917 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1918 self.format_width_int.pack(side=TOP, padx=10, pady=5)
1919 # frame_context.
1920 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
1921 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1922 self.context_int.pack(side=TOP, padx=5, pady=5)
1923
csabellae8eb17b2017-07-30 18:39:17 -04001924 # frame_help.
1925 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1926 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1927 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1928 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1929 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1930 self.button_helplist_add.pack(side=TOP, anchor=W)
1931 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1932
1933 def load_general_cfg(self):
1934 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05001935 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04001936 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05001937 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04001938 self.win_width.set(idleConf.GetOption(
1939 'main', 'EditorWindow', 'width', type='int'))
1940 self.win_height.set(idleConf.GetOption(
1941 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05001942 self.autocomplete_wait.set(idleConf.GetOption(
1943 'extensions', 'AutoComplete', 'popupwait', type='int'))
1944 self.paren_style.set(idleConf.GetOption(
1945 'extensions', 'ParenMatch', 'style'))
1946 self.flash_delay.set(idleConf.GetOption(
1947 'extensions', 'ParenMatch', 'flash-delay', type='int'))
1948 self.paren_bell.set(idleConf.GetOption(
1949 'extensions', 'ParenMatch', 'bell'))
1950
1951 # Set variables for editor windows.
1952 self.autosave.set(idleConf.GetOption(
1953 'main', 'General', 'autosave', default=0, type='bool'))
1954 self.format_width.set(idleConf.GetOption(
1955 'extensions', 'FormatParagraph', 'max-width', type='int'))
1956 self.context_lines.set(idleConf.GetOption(
1957 'extensions', 'CodeContext', 'numlines', type='int'))
1958
csabellae8eb17b2017-07-30 18:39:17 -04001959 # Set additional help sources.
1960 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
1961 self.helplist.delete(0, 'end')
1962 for help_item in self.user_helplist:
1963 self.helplist.insert(END, help_item[0])
1964 self.set_add_delete_state()
1965
1966 def help_source_selected(self, event):
1967 "Handle event for selecting additional help."
1968 self.set_add_delete_state()
1969
1970 def set_add_delete_state(self):
1971 "Toggle the state for the help list buttons based on list entries."
1972 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001973 self.button_helplist_edit.state(('disabled',))
1974 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04001975 else: # Some entries.
1976 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001977 self.button_helplist_edit.state(('!disabled',))
1978 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04001979 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001980 self.button_helplist_edit.state(('disabled',))
1981 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04001982
1983 def helplist_item_add(self):
1984 """Handle add button for the help list.
1985
1986 Query for name and location of new help sources and add
1987 them to the list.
1988 """
1989 help_source = HelpSource(self, 'New Help Source').result
1990 if help_source:
1991 self.user_helplist.append(help_source)
1992 self.helplist.insert(END, help_source[0])
1993 self.update_help_changes()
1994
1995 def helplist_item_edit(self):
1996 """Handle edit button for the help list.
1997
1998 Query with existing help source information and update
1999 config if the values are changed.
2000 """
2001 item_index = self.helplist.index(ANCHOR)
2002 help_source = self.user_helplist[item_index]
2003 new_help_source = HelpSource(
2004 self, 'Edit Help Source',
2005 menuitem=help_source[0],
2006 filepath=help_source[1],
2007 ).result
2008 if new_help_source and new_help_source != help_source:
2009 self.user_helplist[item_index] = new_help_source
2010 self.helplist.delete(item_index)
2011 self.helplist.insert(item_index, new_help_source[0])
2012 self.update_help_changes()
2013 self.set_add_delete_state() # Selected will be un-selected
2014
2015 def helplist_item_remove(self):
2016 """Handle remove button for the help list.
2017
2018 Delete the help list item from config.
2019 """
2020 item_index = self.helplist.index(ANCHOR)
2021 del(self.user_helplist[item_index])
2022 self.helplist.delete(item_index)
2023 self.update_help_changes()
2024 self.set_add_delete_state()
2025
2026 def update_help_changes(self):
2027 "Clear and rebuild the HelpFiles section in changes"
2028 changes['main']['HelpFiles'] = {}
2029 for num in range(1, len(self.user_helplist) + 1):
2030 changes.add_option(
2031 'main', 'HelpFiles', str(num),
2032 ';'.join(self.user_helplist[num-1][:2]))
2033
2034
csabella45bf7232017-07-26 19:09:58 -04002035class VarTrace:
2036 """Maintain Tk variables trace state."""
2037
2038 def __init__(self):
2039 """Store Tk variables and callbacks.
2040
2041 untraced: List of tuples (var, callback)
2042 that do not have the callback attached
2043 to the Tk var.
2044 traced: List of tuples (var, callback) where
2045 that callback has been attached to the var.
2046 """
2047 self.untraced = []
2048 self.traced = []
2049
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002050 def clear(self):
2051 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002052 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002053 self.untraced.clear()
2054 self.traced.clear()
2055
csabella45bf7232017-07-26 19:09:58 -04002056 def add(self, var, callback):
2057 """Add (var, callback) tuple to untraced list.
2058
2059 Args:
2060 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002061 callback: Either function name to be used as a callback
2062 or a tuple with IdleConf config-type, section, and
2063 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002064
2065 Return:
2066 Tk variable instance.
2067 """
2068 if isinstance(callback, tuple):
2069 callback = self.make_callback(var, callback)
2070 self.untraced.append((var, callback))
2071 return var
2072
2073 @staticmethod
2074 def make_callback(var, config):
2075 "Return default callback function to add values to changes instance."
2076 def default_callback(*params):
2077 "Add config values to changes instance."
2078 changes.add_option(*config, var.get())
2079 return default_callback
2080
2081 def attach(self):
2082 "Attach callback to all vars that are not traced."
2083 while self.untraced:
2084 var, callback = self.untraced.pop()
2085 var.trace_add('write', callback)
2086 self.traced.append((var, callback))
2087
2088 def detach(self):
2089 "Remove callback from traced vars."
2090 while self.traced:
2091 var, callback = self.traced.pop()
2092 var.trace_remove('write', var.trace_info()[0][1])
2093 self.untraced.append((var, callback))
2094
2095
csabella5b591542017-07-28 14:40:59 -04002096tracers = VarTrace()
2097
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002098help_common = '''\
2099When you click either the Apply or Ok buttons, settings in this
2100dialog that are different from IDLE's default are saved in
2101a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002102these changes apply to all versions of IDLE installed on this
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002103machine. Some do not take affect until IDLE is restarted.
2104[Cancel] only cancels changes made since the last save.
2105'''
2106help_pages = {
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002107 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002108Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002109The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002110be used with older IDLE releases if it is saved as a custom
2111theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002112''',
2113 'Keys': '''
2114Keys:
2115The IDLE Modern Unix key set is new in June 2016. It can only
2116be used with older IDLE releases if it is saved as a custom
2117key set, with a different name.
2118''',
wohlganger58fc71c2017-09-10 16:19:47 -05002119 'General': '''
2120General:
wohlgangerfae2c352017-06-27 21:36:23 -05002121
wohlganger58fc71c2017-09-10 16:19:47 -05002122AutoComplete: Popupwait is milleseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002123cursor movement, before popping up completion box. Key char is '.' after
2124identifier or a '/' (or '\\' on Windows) within a string.
2125
2126FormatParagraph: Max-width is max chars in lines after re-formatting.
2127Use with paragraphs in both strings and comment blocks.
2128
2129ParenMatch: Style indicates what is highlighted when closer is entered:
2130'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2131'expression' (default) - also everything in between. Flash-delay is how
2132long to highlight if cursor is not moved (0 means forever).
2133'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002134}
2135
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002136
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002137def is_int(s):
2138 "Return 's is blank or represents an int'"
2139 if not s:
2140 return True
2141 try:
2142 int(s)
2143 return True
2144 except ValueError:
2145 return False
2146
2147
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002148class VerticalScrolledFrame(Frame):
2149 """A pure Tkinter vertically scrollable frame.
2150
2151 * Use the 'interior' attribute to place widgets inside the scrollable frame
2152 * Construct and pack/place/grid normally
2153 * This frame only allows vertical scrolling
2154 """
2155 def __init__(self, parent, *args, **kw):
2156 Frame.__init__(self, parent, *args, **kw)
2157
csabella7eb58832017-07-04 21:30:58 -04002158 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002159 vscrollbar = Scrollbar(self, orient=VERTICAL)
2160 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002161 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002162 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002163 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2164 vscrollbar.config(command=canvas.yview)
2165
csabella7eb58832017-07-04 21:30:58 -04002166 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002167 canvas.xview_moveto(0)
2168 canvas.yview_moveto(0)
2169
csabella7eb58832017-07-04 21:30:58 -04002170 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002171 self.interior = interior = Frame(canvas)
2172 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2173
csabella7eb58832017-07-04 21:30:58 -04002174 # Track changes to the canvas and frame width and sync them,
2175 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002176 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002177 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002178 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2179 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002180 interior.bind('<Configure>', _configure_interior)
2181
2182 def _configure_canvas(event):
2183 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002184 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002185 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2186 canvas.bind('<Configure>', _configure_canvas)
2187
2188 return
2189
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002190
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002191if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002192 import unittest
2193 unittest.main('idlelib.idle_test.test_configdialog',
2194 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002195 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002196 run(ConfigDialog)