blob: 099f5262b1e4f5052888ab572cad043e9b1b1e50 [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,
Terry Jan Reedye2e42272017-10-17 18:56:16 -040015 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW, CENTER,
Louie Lubb2bae82017-07-10 06:57:18 +080016 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
Cheryl Sabella7028e592017-08-26 14:26:02 -040017from tkinter.ttk import (Button, Checkbutton, Entry, Frame, Label, LabelFrame,
wohlganger58fc71c2017-09-10 16:19:47 -050018 OptionMenu, Notebook, Radiobutton, Scrollbar, Style)
Georg Brandl14fc4272008-05-17 18:39:55 +000019import tkinter.colorchooser as tkColorChooser
20import tkinter.font as tkFont
Terry Jan Reedy3457f422017-08-27 16:39:41 -040021from tkinter import messagebox
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000022
terryjreedy349abd92017-07-07 16:00:57 -040023from idlelib.config import idleConf, ConfigChanges
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040024from idlelib.config_key import GetKeysDialog
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040025from idlelib.dynoption import DynOptionMenu
26from idlelib import macosx
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040027from idlelib.query import SectionName, HelpSource
Terry Jan Reedya9421fb2014-10-22 20:15:18 -040028from idlelib.tabbedpages import TabbedPageSet
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040029from idlelib.textview import view_text
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040030from idlelib.autocomplete import AutoComplete
31from idlelib.codecontext import CodeContext
32from idlelib.parenmatch import ParenMatch
33from idlelib.paragraph import FormatParagraph
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -040034
terryjreedy349abd92017-07-07 16:00:57 -040035changes = ConfigChanges()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040036# Reload changed options in the following classes.
37reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph)
terryjreedy349abd92017-07-07 16:00:57 -040038
csabella5b591542017-07-28 14:40:59 -040039
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000040class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -040041 """Config dialog for IDLE.
42 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000043
Terry Jan Reedybfebfd82017-09-30 17:37:53 -040044 def __init__(self, parent, title='', *, _htest=False, _utest=False):
csabella7eb58832017-07-04 21:30:58 -040045 """Show the tabbed dialog for user configuration.
46
csabella36329a42017-07-13 23:32:01 -040047 Args:
48 parent - parent of this dialog
49 title - string which is the title of this popup dialog
50 _htest - bool, change box location when running htest
51 _utest - bool, don't wait_window when running unittest
52
53 Note: Focus set on font page fontlist.
54
55 Methods:
56 create_widgets
57 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040058 """
Steven M. Gavad721c482001-07-31 10:46:53 +000059 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040060 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040061 if _htest:
62 parent.instance_dict = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080063 if not _utest:
64 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000065
Steven M. Gavad721c482001-07-31 10:46:53 +000066 self.configure(borderwidth=5)
Terry Jan Reedycd567362014-10-17 01:31:35 -040067 self.title(title or 'IDLE Preferences')
csabellabac7d332017-06-26 17:46:26 -040068 x = parent.winfo_rootx() + 20
69 y = parent.winfo_rooty() + (30 if not _htest else 150)
70 self.geometry(f'+{x}+{y}')
csabella7eb58832017-07-04 21:30:58 -040071 # Each theme element key is its display name.
72 # The first value of the tuple is the sample area tag name.
73 # The second value is the display name list sort index.
csabellabac7d332017-06-26 17:46:26 -040074 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040075 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000076 self.transient(parent)
csabellabac7d332017-06-26 17:46:26 -040077 self.protocol("WM_DELETE_WINDOW", self.cancel)
csabella9397e2a2017-07-30 13:34:25 -040078 self.fontpage.fontlist.focus_set()
csabella7eb58832017-07-04 21:30:58 -040079 # XXX Decide whether to keep or delete these key bindings.
80 # Key bindings for this dialog.
81 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
82 # self.bind('<Alt-a>', self.Apply) #apply changes, save
83 # self.bind('<F1>', self.Help) #context help
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040084 # Attach callbacks after loading config to avoid calling them.
csabella5b591542017-07-28 14:40:59 -040085 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000086
Terry Jan Reedycfa89502014-07-14 23:07:32 -040087 if not _utest:
Louie Lu9b622fb2017-07-14 08:35:48 +080088 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040089 self.wm_deiconify()
90 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000091
csabellabac7d332017-06-26 17:46:26 -040092 def create_widgets(self):
csabella36329a42017-07-13 23:32:01 -040093 """Create and place widgets for tabbed dialog.
94
95 Widgets Bound to self:
csabellae8eb17b2017-07-30 18:39:17 -040096 note: Notebook
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040097 highpage: HighPage
csabellae8eb17b2017-07-30 18:39:17 -040098 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -040099 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -0400100 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400101 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -0400102
103 Methods:
csabella36329a42017-07-13 23:32:01 -0400104 create_action_buttons
105 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400106 activate_config_changes: Tell editors to reload.
107 """
Terry Jan Reedyd6e2f262017-09-19 19:01:45 -0400108 self.note = note = Notebook(self)
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400109 self.highpage = HighPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400110 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400111 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400112 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400113 self.extpage = self.create_page_extensions()
114 note.add(self.fontpage, text='Fonts/Tabs')
115 note.add(self.highpage, text='Highlights')
116 note.add(self.keyspage, text=' Keys ')
117 note.add(self.genpage, text=' General ')
118 note.add(self.extpage, text='Extensions')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400119 note.enable_traversal()
120 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400121 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400122
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400123 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -0400124 """Return frame of action buttons for dialog.
125
126 Methods:
127 ok
128 apply
129 cancel
130 help
131
132 Widget Structure:
133 outer: Frame
134 buttons: Frame
135 (no assignment): Button (ok)
136 (no assignment): Button (apply)
137 (no assignment): Button (cancel)
138 (no assignment): Button (help)
139 (no assignment): Frame
140 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400141 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400142 # Changing the default padding on OSX results in unreadable
csabella7eb58832017-07-04 21:30:58 -0400143 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400144 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000145 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400146 padding_args = {'padding': (6, 3)}
147 outer = Frame(self, padding=2)
148 buttons = Frame(outer, padding=2)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400149 for txt, cmd in (
csabellabac7d332017-06-26 17:46:26 -0400150 ('Ok', self.ok),
151 ('Apply', self.apply),
152 ('Cancel', self.cancel),
153 ('Help', self.help)):
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400154 Button(buttons, text=txt, command=cmd, takefocus=FALSE,
csabellabac7d332017-06-26 17:46:26 -0400155 **padding_args).pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -0400156 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400157 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
158 buttons.pack(side=BOTTOM)
159 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400160
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400161 def ok(self):
162 """Apply config changes, then dismiss dialog.
163
164 Methods:
165 apply
166 destroy: inherited
167 """
168 self.apply()
169 self.destroy()
170
171 def apply(self):
172 """Apply config changes and leave dialog open.
173
174 Methods:
175 deactivate_current_config
176 save_all_changed_extensions
177 activate_config_changes
178 """
179 self.deactivate_current_config()
180 changes.save_all()
181 self.save_all_changed_extensions()
182 self.activate_config_changes()
183
184 def cancel(self):
185 """Dismiss config dialog.
186
187 Methods:
188 destroy: inherited
189 """
190 self.destroy()
191
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300192 def destroy(self):
193 global font_sample_text
194 font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
195 super().destroy()
196
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400197 def help(self):
198 """Create textview for config dialog help.
199
200 Attrbutes accessed:
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400201 note
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400202
203 Methods:
204 view_text: Method from textview module.
205 """
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400206 page = self.note.tab(self.note.select(), option='text').strip()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400207 view_text(self, title='Help for IDLE preferences',
208 text=help_common+help_pages.get(page, ''))
209
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400210 def deactivate_current_config(self):
211 """Remove current key bindings.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400212 Iterate over window instances defined in parent and remove
213 the keybindings.
214 """
215 # Before a config is saved, some cleanup of current
216 # config must be done - remove the previous keybindings.
217 win_instances = self.parent.instance_dict.keys()
218 for instance in win_instances:
219 instance.RemoveKeybindings()
220
221 def activate_config_changes(self):
222 """Apply configuration changes to current windows.
223
224 Dynamically update the current parent window instances
225 with some of the configuration changes.
226 """
227 win_instances = self.parent.instance_dict.keys()
228 for instance in win_instances:
229 instance.ResetColorizer()
230 instance.ResetFont()
231 instance.set_notabs_indentwidth()
232 instance.ApplyKeybindings()
233 instance.reset_help_menu_entries()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -0400234 for klass in reloadables:
235 klass.reload()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400236
csabellabac7d332017-06-26 17:46:26 -0400237 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400238 """Part of the config dialog used for configuring IDLE extensions.
239
240 This code is generic - it works for any and all IDLE extensions.
241
242 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400243 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400244 GUI interface to change the configuration values, and saves the
245 changes using idleConf.
246
247 Not all changes take effect immediately - some may require restarting IDLE.
248 This depends on each extension's implementation.
249
250 All values are treated as text, and it is up to the user to supply
251 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300252 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -0400253
254 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300255 load_extensions:
csabella36329a42017-07-13 23:32:01 -0400256 extension_selected: Handle selection from list.
257 create_extension_frame: Hold widgets for one extension.
258 set_extension_value: Set in userCfg['extensions'].
259 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400260 """
261 parent = self.parent
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400262 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400263 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
264 self.ext_userCfg = idleConf.userCfg['extensions']
265 self.is_int = self.register(is_int)
266 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -0400267 # Create widgets - a listbox shows all available extensions, with the
268 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400269 self.extension_names = StringVar(self)
270 frame.rowconfigure(0, weight=1)
271 frame.columnconfigure(2, weight=1)
272 self.extension_list = Listbox(frame, listvariable=self.extension_names,
273 selectmode='browse')
274 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
275 scroll = Scrollbar(frame, command=self.extension_list.yview)
276 self.extension_list.yscrollcommand=scroll.set
277 self.details_frame = LabelFrame(frame, width=250, height=250)
278 self.extension_list.grid(column=0, row=0, sticky='nws')
279 scroll.grid(column=1, row=0, sticky='ns')
280 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
Cheryl Sabella7028e592017-08-26 14:26:02 -0400281 frame.configure(padding=10)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400282 self.config_frame = {}
283 self.current_extension = None
284
285 self.outerframe = self # TEMPORARY
286 self.tabbed_page_set = self.extension_list # TEMPORARY
287
csabella7eb58832017-07-04 21:30:58 -0400288 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400289 ext_names = ''
290 for ext_name in sorted(self.extensions):
291 self.create_extension_frame(ext_name)
292 ext_names = ext_names + '{' + ext_name + '} '
293 self.extension_names.set(ext_names)
294 self.extension_list.selection_set(0)
295 self.extension_selected(None)
296
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400297 return frame
298
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400299 def load_extensions(self):
300 "Fill self.extensions with data from the default and user configs."
301 self.extensions = {}
302 for ext_name in idleConf.GetExtensions(active_only=False):
wohlganger58fc71c2017-09-10 16:19:47 -0500303 # Former built-in extensions are already filtered out.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400304 self.extensions[ext_name] = []
305
306 for ext_name in self.extensions:
307 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
308
csabella7eb58832017-07-04 21:30:58 -0400309 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400310 enables = [opt_name for opt_name in opt_list
311 if opt_name.startswith('enable')]
312 for opt_name in enables:
313 opt_list.remove(opt_name)
314 opt_list = enables + opt_list
315
316 for opt_name in opt_list:
317 def_str = self.ext_defaultCfg.Get(
318 ext_name, opt_name, raw=True)
319 try:
320 def_obj = {'True':True, 'False':False}[def_str]
321 opt_type = 'bool'
322 except KeyError:
323 try:
324 def_obj = int(def_str)
325 opt_type = 'int'
326 except ValueError:
327 def_obj = def_str
328 opt_type = None
329 try:
330 value = self.ext_userCfg.Get(
331 ext_name, opt_name, type=opt_type, raw=True,
332 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -0400333 except ValueError: # Need this until .Get fixed.
334 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400335 var = StringVar(self)
336 var.set(str(value))
337
338 self.extensions[ext_name].append({'name': opt_name,
339 'type': opt_type,
340 'default': def_str,
341 'value': value,
342 'var': var,
343 })
344
345 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -0400346 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400347 newsel = self.extension_list.curselection()
348 if newsel:
349 newsel = self.extension_list.get(newsel)
350 if newsel is None or newsel != self.current_extension:
351 if self.current_extension:
352 self.details_frame.config(text='')
353 self.config_frame[self.current_extension].grid_forget()
354 self.current_extension = None
355 if newsel:
356 self.details_frame.config(text=newsel)
357 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
358 self.current_extension = newsel
359
360 def create_extension_frame(self, ext_name):
361 """Create a frame holding the widgets to configure one extension"""
362 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
363 self.config_frame[ext_name] = f
364 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -0400365 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400366 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -0400367 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400368 label = Label(entry_area, text=opt['name'])
369 label.grid(row=row, column=0, sticky=NW)
370 var = opt['var']
371 if opt['type'] == 'bool':
Cheryl Sabella7028e592017-08-26 14:26:02 -0400372 Checkbutton(entry_area, variable=var,
373 onvalue='True', offvalue='False', width=8
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400374 ).grid(row=row, column=1, sticky=W, padx=7)
375 elif opt['type'] == 'int':
376 Entry(entry_area, textvariable=var, validate='key',
377 validatecommand=(self.is_int, '%P')
378 ).grid(row=row, column=1, sticky=NSEW, padx=7)
379
380 else:
381 Entry(entry_area, textvariable=var
382 ).grid(row=row, column=1, sticky=NSEW, padx=7)
383 return
384
385 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400386 """Return True if the configuration was added or changed.
387
388 If the value is the same as the default, then remove it
389 from user config file.
390 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400391 name = opt['name']
392 default = opt['default']
393 value = opt['var'].get().strip() or default
394 opt['var'].set(value)
395 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400396 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400397 if (value == default):
398 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400399 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400400 return self.ext_userCfg.SetOption(section, name, value)
401
402 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400403 """Save configuration changes to the user config file.
404
405 Attributes accessed:
406 extensions
407
408 Methods:
409 set_extension_value
410 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400411 has_changes = False
412 for ext_name in self.extensions:
413 options = self.extensions[ext_name]
414 for opt in options:
415 if self.set_extension_value(ext_name, opt):
416 has_changes = True
417 if has_changes:
418 self.ext_userCfg.Save()
419
420
csabella6f446be2017-08-01 00:24:07 -0400421# class TabPage(Frame): # A template for Page classes.
422# def __init__(self, master):
423# super().__init__(master)
424# self.create_page_tab()
425# self.load_tab_cfg()
426# def create_page_tab(self):
427# # Define tk vars and register var and callback with tracers.
428# # Create subframes and widgets.
429# # Pack widgets.
430# def load_tab_cfg(self):
431# # Initialize widgets with data from idleConf.
432# def var_changed_var_name():
433# # For each tk var that needs other than default callback.
434# def other_methods():
435# # Define tab-specific behavior.
436
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300437font_sample_text = (
438 '<ASCII/Latin1>\n'
439 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
440 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
441 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
442 '\n<IPA,Greek,Cyrillic>\n'
443 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
444 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
445 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
446 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
447 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
448 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
449 '\n<Hebrew, Arabic>\n'
450 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
451 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
452 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
453 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
454 '\n<Devanagari, Tamil>\n'
455 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
456 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
457 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
458 '\u0b85\u0b87\u0b89\u0b8e\n'
459 '\n<East Asian>\n'
460 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
461 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
462 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
463 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
464 )
465
csabella6f446be2017-08-01 00:24:07 -0400466
csabella9397e2a2017-07-30 13:34:25 -0400467class FontPage(Frame):
468
csabella6f446be2017-08-01 00:24:07 -0400469 def __init__(self, master, highpage):
470 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400471 self.highlight_sample = highpage.highlight_sample
472 self.create_page_font_tab()
473 self.load_font_cfg()
474 self.load_tab_cfg()
475
476 def create_page_font_tab(self):
477 """Return frame of widgets for Font/Tabs tab.
478
479 Fonts: Enable users to provisionally change font face, size, or
480 boldness and to see the consequence of proposed choices. Each
481 action set 3 options in changes structuree and changes the
482 corresponding aspect of the font sample on this page and
483 highlight sample on highlight page.
484
485 Function load_font_cfg initializes font vars and widgets from
486 idleConf entries and tk.
487
488 Fontlist: mouse button 1 click or up or down key invoke
489 on_fontlist_select(), which sets var font_name.
490
491 Sizelist: clicking the menubutton opens the dropdown menu. A
492 mouse button 1 click or return key sets var font_size.
493
494 Bold_toggle: clicking the box toggles var font_bold.
495
496 Changing any of the font vars invokes var_changed_font, which
497 adds all 3 font options to changes and calls set_samples.
498 Set_samples applies a new font constructed from the font vars to
499 font_sample and to highlight_sample on the hightlight page.
500
501 Tabs: Enable users to change spaces entered for indent tabs.
502 Changing indent_scale value with the mouse sets Var space_num,
503 which invokes the default callback to add an entry to
504 changes. Load_tab_cfg initializes space_num to default.
505
Cheryl Sabella2f896462017-08-14 21:21:43 -0400506 Widgets for FontPage(Frame): (*) widgets bound to self
507 frame_font: LabelFrame
508 frame_font_name: Frame
509 font_name_title: Label
510 (*)fontlist: ListBox - font_name
511 scroll_font: Scrollbar
512 frame_font_param: Frame
513 font_size_title: Label
514 (*)sizelist: DynOptionMenu - font_size
515 (*)bold_toggle: Checkbutton - font_bold
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400516 frame_sample: LabelFrame
517 (*)font_sample: Label
Cheryl Sabella2f896462017-08-14 21:21:43 -0400518 frame_indent: LabelFrame
519 indent_title: Label
520 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400521 """
csabella6f446be2017-08-01 00:24:07 -0400522 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
523 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
524 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400525 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
526
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400527 # Define frames and widgets.
csabella9397e2a2017-07-30 13:34:25 -0400528 frame_font = LabelFrame(
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400529 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
530 frame_sample = LabelFrame(
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300531 self, borderwidth=2, relief=GROOVE,
532 text=' Font Sample (Editable) ')
csabella9397e2a2017-07-30 13:34:25 -0400533 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400534 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400535 # frame_font.
536 frame_font_name = Frame(frame_font)
537 frame_font_param = Frame(frame_font)
538 font_name_title = Label(
539 frame_font_name, justify=LEFT, text='Font Face :')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400540 self.fontlist = Listbox(frame_font_name, height=15,
csabella9397e2a2017-07-30 13:34:25 -0400541 takefocus=True, exportselection=FALSE)
542 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
543 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
544 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
545 scroll_font = Scrollbar(frame_font_name)
546 scroll_font.config(command=self.fontlist.yview)
547 self.fontlist.config(yscrollcommand=scroll_font.set)
548 font_size_title = Label(frame_font_param, text='Size :')
549 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
550 self.bold_toggle = Checkbutton(
551 frame_font_param, variable=self.font_bold,
552 onvalue=1, offvalue=0, text='Bold')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400553 # frame_sample.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300554 self.font_sample = Text(frame_sample, width=20, height=20)
555 self.font_sample.insert(END, font_sample_text)
csabella9397e2a2017-07-30 13:34:25 -0400556 # frame_indent.
557 indent_title = Label(
558 frame_indent, justify=LEFT,
559 text='Python Standard: 4 Spaces!')
560 self.indent_scale = Scale(
561 frame_indent, variable=self.space_num,
562 orient='horizontal', tickinterval=2, from_=2, to=16)
563
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400564 # Grid and pack widgets:
565 self.columnconfigure(1, weight=1)
566 frame_font.grid(row=0, column=0, padx=5, pady=5)
567 frame_sample.grid(row=0, column=1, rowspan=2, padx=5, pady=5,
568 sticky='nsew')
569 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
csabella9397e2a2017-07-30 13:34:25 -0400570 # frame_font.
571 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
572 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
573 font_name_title.pack(side=TOP, anchor=W)
574 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
575 scroll_font.pack(side=LEFT, fill=Y)
576 font_size_title.pack(side=LEFT, anchor=W)
577 self.sizelist.pack(side=LEFT, anchor=W)
578 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400579 # frame_sample.
csabella9397e2a2017-07-30 13:34:25 -0400580 self.font_sample.pack(expand=TRUE, fill=BOTH)
581 # frame_indent.
csabella9397e2a2017-07-30 13:34:25 -0400582 indent_title.pack(side=TOP, anchor=W, padx=5)
583 self.indent_scale.pack(side=TOP, padx=5, fill=X)
584
csabella9397e2a2017-07-30 13:34:25 -0400585 def load_font_cfg(self):
586 """Load current configuration settings for the font options.
587
588 Retrieve current font with idleConf.GetFont and font families
589 from tk. Setup fontlist and set font_name. Setup sizelist,
590 which sets font_size. Set font_bold. Call set_samples.
591 """
592 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
593 font_name = configured_font[0].lower()
594 font_size = configured_font[1]
595 font_bold = configured_font[2]=='bold'
596
597 # Set editor font selection list and font_name.
598 fonts = list(tkFont.families(self))
599 fonts.sort()
600 for font in fonts:
601 self.fontlist.insert(END, font)
602 self.font_name.set(font_name)
603 lc_fonts = [s.lower() for s in fonts]
604 try:
605 current_font_index = lc_fonts.index(font_name)
606 self.fontlist.see(current_font_index)
607 self.fontlist.select_set(current_font_index)
608 self.fontlist.select_anchor(current_font_index)
609 self.fontlist.activate(current_font_index)
610 except ValueError:
611 pass
612 # Set font size dropdown.
613 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
614 '16', '18', '20', '22', '25', '29', '34', '40'),
615 font_size)
616 # Set font weight.
617 self.font_bold.set(font_bold)
618 self.set_samples()
619
620 def var_changed_font(self, *params):
621 """Store changes to font attributes.
622
623 When one font attribute changes, save them all, as they are
624 not independent from each other. In particular, when we are
625 overriding the default font, we need to write out everything.
626 """
627 value = self.font_name.get()
628 changes.add_option('main', 'EditorWindow', 'font', value)
629 value = self.font_size.get()
630 changes.add_option('main', 'EditorWindow', 'font-size', value)
631 value = self.font_bold.get()
632 changes.add_option('main', 'EditorWindow', 'font-bold', value)
633 self.set_samples()
634
635 def on_fontlist_select(self, event):
636 """Handle selecting a font from the list.
637
638 Event can result from either mouse click or Up or Down key.
639 Set font_name and example displays to selection.
640 """
641 font = self.fontlist.get(
642 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
643 self.font_name.set(font.lower())
644
645 def set_samples(self, event=None):
646 """Update update both screen samples with the font settings.
647
648 Called on font initialization and change events.
649 Accesses font_name, font_size, and font_bold Variables.
650 Updates font_sample and hightlight page highlight_sample.
651 """
652 font_name = self.font_name.get()
653 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
654 new_font = (font_name, self.font_size.get(), font_weight)
655 self.font_sample['font'] = new_font
656 self.highlight_sample['font'] = new_font
657
658 def load_tab_cfg(self):
659 """Load current configuration settings for the tab options.
660
661 Attributes updated:
662 space_num: Set to value from idleConf.
663 """
664 # Set indent sizes.
665 space_num = idleConf.GetOption(
666 'main', 'Indent', 'num-spaces', default=4, type='int')
667 self.space_num.set(space_num)
668
669 def var_changed_space_num(self, *params):
670 "Store change to indentation size."
671 value = self.space_num.get()
672 changes.add_option('main', 'Indent', 'num-spaces', value)
673
674
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400675class HighPage(Frame):
676
677 def __init__(self, master):
678 super().__init__(master)
679 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400680 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400681 self.create_page_highlight()
682 self.load_theme_cfg()
683
684 def create_page_highlight(self):
685 """Return frame of widgets for Highlighting tab.
686
687 Enable users to provisionally change foreground and background
688 colors applied to textual tags. Color mappings are stored in
689 complete listings called themes. Built-in themes in
690 idlelib/config-highlight.def are fixed as far as the dialog is
691 concerned. Any theme can be used as the base for a new custom
692 theme, stored in .idlerc/config-highlight.cfg.
693
694 Function load_theme_cfg() initializes tk variables and theme
695 lists and calls paint_theme_sample() and set_highlight_target()
696 for the current theme. Radiobuttons builtin_theme_on and
697 custom_theme_on toggle var theme_source, which controls if the
698 current set of colors are from a builtin or custom theme.
699 DynOptionMenus builtinlist and customlist contain lists of the
700 builtin and custom themes, respectively, and the current item
701 from each list is stored in vars builtin_name and custom_name.
702
703 Function paint_theme_sample() applies the colors from the theme
704 to the tags in text widget highlight_sample and then invokes
705 set_color_sample(). Function set_highlight_target() sets the state
706 of the radiobuttons fg_on and bg_on based on the tag and it also
707 invokes set_color_sample().
708
709 Function set_color_sample() sets the background color for the frame
710 holding the color selector. This provides a larger visual of the
711 color for the current tag and plane (foreground/background).
712
713 Note: set_color_sample() is called from many places and is often
714 called more than once when a change is made. It is invoked when
715 foreground or background is selected (radiobuttons), from
716 paint_theme_sample() (theme is changed or load_cfg is called), and
717 from set_highlight_target() (target tag is changed or load_cfg called).
718
719 Button delete_custom invokes delete_custom() to delete
720 a custom theme from idleConf.userCfg['highlight'] and changes.
721 Button save_custom invokes save_as_new_theme() which calls
722 get_new_theme_name() and create_new() to save a custom theme
723 and its colors to idleConf.userCfg['highlight'].
724
725 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
726 if the current selected color for a tag is for the foreground or
727 background.
728
729 DynOptionMenu targetlist contains a readable description of the
730 tags applied to Python source within IDLE. Selecting one of the
731 tags from this list populates highlight_target, which has a callback
732 function set_highlight_target().
733
734 Text widget highlight_sample displays a block of text (which is
735 mock Python code) in which is embedded the defined tags and reflects
736 the color attributes of the current theme and changes for those tags.
737 Mouse button 1 allows for selection of a tag and updates
738 highlight_target with that tag value.
739
740 Note: The font in highlight_sample is set through the config in
741 the fonts tab.
742
743 In other words, a tag can be selected either from targetlist or
744 by clicking on the sample text within highlight_sample. The
745 plane (foreground/background) is selected via the radiobutton.
746 Together, these two (tag and plane) control what color is
747 shown in set_color_sample() for the current theme. Button set_color
748 invokes get_color() which displays a ColorChooser to change the
749 color for the selected tag/plane. If a new color is picked,
750 it will be saved to changes and the highlight_sample and
751 frame background will be updated.
752
753 Tk Variables:
754 color: Color of selected target.
755 builtin_name: Menu variable for built-in theme.
756 custom_name: Menu variable for custom theme.
757 fg_bg_toggle: Toggle for foreground/background color.
758 Note: this has no callback.
759 theme_source: Selector for built-in or custom theme.
760 highlight_target: Menu variable for the highlight tag target.
761
762 Instance Data Attributes:
763 theme_elements: Dictionary of tags for text highlighting.
764 The key is the display name and the value is a tuple of
765 (tag name, display sort order).
766
767 Methods [attachment]:
768 load_theme_cfg: Load current highlight colors.
769 get_color: Invoke colorchooser [button_set_color].
770 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
771 set_highlight_target: set fg_bg_toggle, set_color_sample().
772 set_color_sample: Set frame background to target.
773 on_new_color_set: Set new color and add option.
774 paint_theme_sample: Recolor sample.
775 get_new_theme_name: Get from popup.
776 create_new: Combine theme with changes and save.
777 save_as_new_theme: Save [button_save_custom].
778 set_theme_type: Command for [theme_source].
779 delete_custom: Activate default [button_delete_custom].
780 save_new: Save to userCfg['theme'] (is function).
781
782 Widgets of highlights page frame: (*) widgets bound to self
783 frame_custom: LabelFrame
784 (*)highlight_sample: Text
785 (*)frame_color_set: Frame
786 (*)button_set_color: Button
787 (*)targetlist: DynOptionMenu - highlight_target
788 frame_fg_bg_toggle: Frame
789 (*)fg_on: Radiobutton - fg_bg_toggle
790 (*)bg_on: Radiobutton - fg_bg_toggle
791 (*)button_save_custom: Button
792 frame_theme: LabelFrame
793 theme_type_title: Label
794 (*)builtin_theme_on: Radiobutton - theme_source
795 (*)custom_theme_on: Radiobutton - theme_source
796 (*)builtinlist: DynOptionMenu - builtin_name
797 (*)customlist: DynOptionMenu - custom_name
798 (*)button_delete_custom: Button
799 (*)theme_message: Label
800 """
801 self.theme_elements = {
802 'Normal Text': ('normal', '00'),
803 'Python Keywords': ('keyword', '01'),
804 'Python Definitions': ('definition', '02'),
805 'Python Builtins': ('builtin', '03'),
806 'Python Comments': ('comment', '04'),
807 'Python Strings': ('string', '05'),
808 'Selected Text': ('hilite', '06'),
809 'Found Text': ('hit', '07'),
810 'Cursor': ('cursor', '08'),
811 'Editor Breakpoint': ('break', '09'),
812 'Shell Normal Text': ('console', '10'),
813 'Shell Error Text': ('error', '11'),
814 'Shell Stdout Text': ('stdout', '12'),
815 'Shell Stderr Text': ('stderr', '13'),
816 }
817 self.builtin_name = tracers.add(
818 StringVar(self), self.var_changed_builtin_name)
819 self.custom_name = tracers.add(
820 StringVar(self), self.var_changed_custom_name)
821 self.fg_bg_toggle = BooleanVar(self)
822 self.color = tracers.add(
823 StringVar(self), self.var_changed_color)
824 self.theme_source = tracers.add(
825 BooleanVar(self), self.var_changed_theme_source)
826 self.highlight_target = tracers.add(
827 StringVar(self), self.var_changed_highlight_target)
828
829 # Create widgets:
830 # body frame and section frames.
831 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
832 text=' Custom Highlighting ')
833 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
834 text=' Highlighting Theme ')
835 # frame_custom.
836 text = self.highlight_sample = Text(
837 frame_custom, relief=SOLID, borderwidth=1,
838 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
839 takefocus=FALSE, highlightthickness=0, wrap=NONE)
840 text.bind('<Double-Button-1>', lambda e: 'break')
841 text.bind('<B1-Motion>', lambda e: 'break')
wohlganger58fc71c2017-09-10 16:19:47 -0500842 text_and_tags=(
843 ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400844 ('#you can click here', 'comment'), ('\n', 'normal'),
845 ('#to choose items', 'comment'), ('\n', 'normal'),
846 ('def', 'keyword'), (' ', 'normal'),
847 ('func', 'definition'), ('(param):\n ', 'normal'),
848 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
849 ("'string'", 'string'), ('\n var1 = ', 'normal'),
850 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
851 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
852 ('list', 'builtin'), ('(', 'normal'),
853 ('None', 'keyword'), (')\n', 'normal'),
854 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
855 (' error ', 'error'), (' ', 'normal'),
856 ('cursor |', 'cursor'), ('\n ', 'normal'),
857 ('shell', 'console'), (' ', 'normal'),
858 ('stdout', 'stdout'), (' ', 'normal'),
859 ('stderr', 'stderr'), ('\n\n', 'normal'))
860 for texttag in text_and_tags:
861 text.insert(END, texttag[0], texttag[1])
862 for element in self.theme_elements:
863 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400864 # event.widget.winfo_top_level().highlight_target.set(elem)
865 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400866 text.tag_bind(
867 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400868 text['state'] = 'disabled'
869 self.style.configure('frame_color_set.TFrame', borderwidth=1,
870 relief='solid')
871 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400872 frame_fg_bg_toggle = Frame(frame_custom)
873 self.button_set_color = Button(
874 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400875 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400876 self.targetlist = DynOptionMenu(
877 self.frame_color_set, self.highlight_target, None,
878 highlightthickness=0) #, command=self.set_highlight_targetBinding
879 self.fg_on = Radiobutton(
880 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
881 text='Foreground', command=self.set_color_sample_binding)
882 self.bg_on = Radiobutton(
883 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
884 text='Background', command=self.set_color_sample_binding)
885 self.fg_bg_toggle.set(1)
886 self.button_save_custom = Button(
887 frame_custom, text='Save as New Custom Theme',
888 command=self.save_as_new_theme)
889 # frame_theme.
890 theme_type_title = Label(frame_theme, text='Select : ')
891 self.builtin_theme_on = Radiobutton(
892 frame_theme, variable=self.theme_source, value=1,
893 command=self.set_theme_type, text='a Built-in Theme')
894 self.custom_theme_on = Radiobutton(
895 frame_theme, variable=self.theme_source, value=0,
896 command=self.set_theme_type, text='a Custom Theme')
897 self.builtinlist = DynOptionMenu(
898 frame_theme, self.builtin_name, None, command=None)
899 self.customlist = DynOptionMenu(
900 frame_theme, self.custom_name, None, command=None)
901 self.button_delete_custom = Button(
902 frame_theme, text='Delete Custom Theme',
903 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400904 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400905 # Pack widgets:
906 # body.
907 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500908 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400909 # frame_custom.
910 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
911 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
912 self.highlight_sample.pack(
913 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
914 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
915 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
916 self.fg_on.pack(side=LEFT, anchor=E)
917 self.bg_on.pack(side=RIGHT, anchor=W)
918 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
919 # frame_theme.
920 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
921 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
922 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
923 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
924 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
925 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
926 self.theme_message.pack(side=TOP, fill=X, pady=5)
927
928 def load_theme_cfg(self):
929 """Load current configuration settings for the theme options.
930
931 Based on the theme_source toggle, the theme is set as
932 either builtin or custom and the initial widget values
933 reflect the current settings from idleConf.
934
935 Attributes updated:
936 theme_source: Set from idleConf.
937 builtinlist: List of default themes from idleConf.
938 customlist: List of custom themes from idleConf.
939 custom_theme_on: Disabled if there are no custom themes.
940 custom_theme: Message with additional information.
941 targetlist: Create menu from self.theme_elements.
942
943 Methods:
944 set_theme_type
945 paint_theme_sample
946 set_highlight_target
947 """
948 # Set current theme type radiobutton.
949 self.theme_source.set(idleConf.GetOption(
950 'main', 'Theme', 'default', type='bool', default=1))
951 # Set current theme.
952 current_option = idleConf.CurrentTheme()
953 # Load available theme option menus.
954 if self.theme_source.get(): # Default theme selected.
955 item_list = idleConf.GetSectionList('default', 'highlight')
956 item_list.sort()
957 self.builtinlist.SetMenu(item_list, current_option)
958 item_list = idleConf.GetSectionList('user', 'highlight')
959 item_list.sort()
960 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400961 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400962 self.custom_name.set('- no custom themes -')
963 else:
964 self.customlist.SetMenu(item_list, item_list[0])
965 else: # User theme selected.
966 item_list = idleConf.GetSectionList('user', 'highlight')
967 item_list.sort()
968 self.customlist.SetMenu(item_list, current_option)
969 item_list = idleConf.GetSectionList('default', 'highlight')
970 item_list.sort()
971 self.builtinlist.SetMenu(item_list, item_list[0])
972 self.set_theme_type()
973 # Load theme element option menu.
974 theme_names = list(self.theme_elements.keys())
975 theme_names.sort(key=lambda x: self.theme_elements[x][1])
976 self.targetlist.SetMenu(theme_names, theme_names[0])
977 self.paint_theme_sample()
978 self.set_highlight_target()
979
980 def var_changed_builtin_name(self, *params):
981 """Process new builtin theme selection.
982
983 Add the changed theme's name to the changed_items and recreate
984 the sample with the values from the selected theme.
985 """
986 old_themes = ('IDLE Classic', 'IDLE New')
987 value = self.builtin_name.get()
988 if value not in old_themes:
989 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
990 changes.add_option('main', 'Theme', 'name', old_themes[0])
991 changes.add_option('main', 'Theme', 'name2', value)
992 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400993 else:
994 changes.add_option('main', 'Theme', 'name', value)
995 changes.add_option('main', 'Theme', 'name2', '')
996 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400997 self.paint_theme_sample()
998
999 def var_changed_custom_name(self, *params):
1000 """Process new custom theme selection.
1001
1002 If a new custom theme is selected, add the name to the
1003 changed_items and apply the theme to the sample.
1004 """
1005 value = self.custom_name.get()
1006 if value != '- no custom themes -':
1007 changes.add_option('main', 'Theme', 'name', value)
1008 self.paint_theme_sample()
1009
1010 def var_changed_theme_source(self, *params):
1011 """Process toggle between builtin and custom theme.
1012
1013 Update the default toggle value and apply the newly
1014 selected theme type.
1015 """
1016 value = self.theme_source.get()
1017 changes.add_option('main', 'Theme', 'default', value)
1018 if value:
1019 self.var_changed_builtin_name()
1020 else:
1021 self.var_changed_custom_name()
1022
1023 def var_changed_color(self, *params):
1024 "Process change to color choice."
1025 self.on_new_color_set()
1026
1027 def var_changed_highlight_target(self, *params):
1028 "Process selection of new target tag for highlighting."
1029 self.set_highlight_target()
1030
1031 def set_theme_type(self):
1032 """Set available screen options based on builtin or custom theme.
1033
1034 Attributes accessed:
1035 theme_source
1036
1037 Attributes updated:
1038 builtinlist
1039 customlist
1040 button_delete_custom
1041 custom_theme_on
1042
1043 Called from:
1044 handler for builtin_theme_on and custom_theme_on
1045 delete_custom
1046 create_new
1047 load_theme_cfg
1048 """
1049 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001050 self.builtinlist['state'] = 'normal'
1051 self.customlist['state'] = 'disabled'
1052 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001053 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001054 self.builtinlist['state'] = 'disabled'
1055 self.custom_theme_on.state(('!disabled',))
1056 self.customlist['state'] = 'normal'
1057 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001058
1059 def get_color(self):
1060 """Handle button to select a new color for the target tag.
1061
1062 If a new color is selected while using a builtin theme, a
1063 name must be supplied to create a custom theme.
1064
1065 Attributes accessed:
1066 highlight_target
1067 frame_color_set
1068 theme_source
1069
1070 Attributes updated:
1071 color
1072
1073 Methods:
1074 get_new_theme_name
1075 create_new
1076 """
1077 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001078 prev_color = self.style.lookup(self.frame_color_set['style'],
1079 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001080 rgbTuplet, color_string = tkColorChooser.askcolor(
1081 parent=self, title='Pick new color for : '+target,
1082 initialcolor=prev_color)
1083 if color_string and (color_string != prev_color):
1084 # User didn't cancel and they chose a new color.
1085 if self.theme_source.get(): # Current theme is a built-in.
1086 message = ('Your changes will be saved as a new Custom Theme. '
1087 'Enter a name for your new Custom Theme below.')
1088 new_theme = self.get_new_theme_name(message)
1089 if not new_theme: # User cancelled custom theme creation.
1090 return
1091 else: # Create new custom theme based on previously active theme.
1092 self.create_new(new_theme)
1093 self.color.set(color_string)
1094 else: # Current theme is user defined.
1095 self.color.set(color_string)
1096
1097 def on_new_color_set(self):
1098 "Display sample of new color selection on the dialog."
1099 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001100 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001101 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1102 sample_element = self.theme_elements[self.highlight_target.get()][0]
1103 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1104 theme = self.custom_name.get()
1105 theme_element = sample_element + '-' + plane
1106 changes.add_option('highlight', theme, theme_element, new_color)
1107
1108 def get_new_theme_name(self, message):
1109 "Return name of new theme from query popup."
1110 used_names = (idleConf.GetSectionList('user', 'highlight') +
1111 idleConf.GetSectionList('default', 'highlight'))
1112 new_theme = SectionName(
1113 self, 'New Custom Theme', message, used_names).result
1114 return new_theme
1115
1116 def save_as_new_theme(self):
1117 """Prompt for new theme name and create the theme.
1118
1119 Methods:
1120 get_new_theme_name
1121 create_new
1122 """
1123 new_theme_name = self.get_new_theme_name('New Theme Name:')
1124 if new_theme_name:
1125 self.create_new(new_theme_name)
1126
1127 def create_new(self, new_theme_name):
1128 """Create a new custom theme with the given name.
1129
1130 Create the new theme based on the previously active theme
1131 with the current changes applied. Once it is saved, then
1132 activate the new theme.
1133
1134 Attributes accessed:
1135 builtin_name
1136 custom_name
1137
1138 Attributes updated:
1139 customlist
1140 theme_source
1141
1142 Method:
1143 save_new
1144 set_theme_type
1145 """
1146 if self.theme_source.get():
1147 theme_type = 'default'
1148 theme_name = self.builtin_name.get()
1149 else:
1150 theme_type = 'user'
1151 theme_name = self.custom_name.get()
1152 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1153 # Apply any of the old theme's unsaved changes to the new theme.
1154 if theme_name in changes['highlight']:
1155 theme_changes = changes['highlight'][theme_name]
1156 for element in theme_changes:
1157 new_theme[element] = theme_changes[element]
1158 # Save the new theme.
1159 self.save_new(new_theme_name, new_theme)
1160 # Change GUI over to the new theme.
1161 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1162 custom_theme_list.sort()
1163 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1164 self.theme_source.set(0)
1165 self.set_theme_type()
1166
1167 def set_highlight_target(self):
1168 """Set fg/bg toggle and color based on highlight tag target.
1169
1170 Instance variables accessed:
1171 highlight_target
1172
1173 Attributes updated:
1174 fg_on
1175 bg_on
1176 fg_bg_toggle
1177
1178 Methods:
1179 set_color_sample
1180
1181 Called from:
1182 var_changed_highlight_target
1183 load_theme_cfg
1184 """
1185 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001186 self.fg_on.state(('disabled',))
1187 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001188 self.fg_bg_toggle.set(1)
1189 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001190 self.fg_on.state(('!disabled',))
1191 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001192 self.fg_bg_toggle.set(1)
1193 self.set_color_sample()
1194
1195 def set_color_sample_binding(self, *args):
1196 """Change color sample based on foreground/background toggle.
1197
1198 Methods:
1199 set_color_sample
1200 """
1201 self.set_color_sample()
1202
1203 def set_color_sample(self):
1204 """Set the color of the frame background to reflect the selected target.
1205
1206 Instance variables accessed:
1207 theme_elements
1208 highlight_target
1209 fg_bg_toggle
1210 highlight_sample
1211
1212 Attributes updated:
1213 frame_color_set
1214 """
1215 # Set the color sample area.
1216 tag = self.theme_elements[self.highlight_target.get()][0]
1217 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1218 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001219 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001220
1221 def paint_theme_sample(self):
1222 """Apply the theme colors to each element tag in the sample text.
1223
1224 Instance attributes accessed:
1225 theme_elements
1226 theme_source
1227 builtin_name
1228 custom_name
1229
1230 Attributes updated:
1231 highlight_sample: Set the tag elements to the theme.
1232
1233 Methods:
1234 set_color_sample
1235
1236 Called from:
1237 var_changed_builtin_name
1238 var_changed_custom_name
1239 load_theme_cfg
1240 """
1241 if self.theme_source.get(): # Default theme
1242 theme = self.builtin_name.get()
1243 else: # User theme
1244 theme = self.custom_name.get()
1245 for element_title in self.theme_elements:
1246 element = self.theme_elements[element_title][0]
1247 colors = idleConf.GetHighlight(theme, element)
1248 if element == 'cursor': # Cursor sample needs special painting.
1249 colors['background'] = idleConf.GetHighlight(
1250 theme, 'normal', fgBg='bg')
1251 # Handle any unsaved changes to this theme.
1252 if theme in changes['highlight']:
1253 theme_dict = changes['highlight'][theme]
1254 if element + '-foreground' in theme_dict:
1255 colors['foreground'] = theme_dict[element + '-foreground']
1256 if element + '-background' in theme_dict:
1257 colors['background'] = theme_dict[element + '-background']
1258 self.highlight_sample.tag_config(element, **colors)
1259 self.set_color_sample()
1260
1261 def save_new(self, theme_name, theme):
1262 """Save a newly created theme to idleConf.
1263
1264 theme_name - string, the name of the new theme
1265 theme - dictionary containing the new theme
1266 """
1267 if not idleConf.userCfg['highlight'].has_section(theme_name):
1268 idleConf.userCfg['highlight'].add_section(theme_name)
1269 for element in theme:
1270 value = theme[element]
1271 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1272
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001273 def askyesno(self, *args, **kwargs):
1274 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001275 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001276
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001277 def delete_custom(self):
1278 """Handle event to delete custom theme.
1279
1280 The current theme is deactivated and the default theme is
1281 activated. The custom theme is permanently removed from
1282 the config file.
1283
1284 Attributes accessed:
1285 custom_name
1286
1287 Attributes updated:
1288 custom_theme_on
1289 customlist
1290 theme_source
1291 builtin_name
1292
1293 Methods:
1294 deactivate_current_config
1295 save_all_changed_extensions
1296 activate_config_changes
1297 set_theme_type
1298 """
1299 theme_name = self.custom_name.get()
1300 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001301 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001302 'Delete Theme', delmsg % theme_name, parent=self):
1303 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001304 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001305 # Remove theme from changes, config, and file.
1306 changes.delete_section('highlight', theme_name)
1307 # Reload user theme list.
1308 item_list = idleConf.GetSectionList('user', 'highlight')
1309 item_list.sort()
1310 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001311 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001312 self.customlist.SetMenu(item_list, '- no custom themes -')
1313 else:
1314 self.customlist.SetMenu(item_list, item_list[0])
1315 # Revert to default theme.
1316 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1317 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1318 # User can't back out of these changes, they must be applied now.
1319 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001320 self.cd.save_all_changed_extensions()
1321 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001322 self.set_theme_type()
1323
1324
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001325class KeysPage(Frame):
1326
1327 def __init__(self, master):
1328 super().__init__(master)
1329 self.cd = master.master
1330 self.create_page_keys()
1331 self.load_key_cfg()
1332
1333 def create_page_keys(self):
1334 """Return frame of widgets for Keys tab.
1335
1336 Enable users to provisionally change both individual and sets of
1337 keybindings (shortcut keys). Except for features implemented as
1338 extensions, keybindings are stored in complete sets called
1339 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1340 as far as the dialog is concerned. Any keyset can be used as the
1341 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1342
1343 Function load_key_cfg() initializes tk variables and keyset
1344 lists and calls load_keys_list for the current keyset.
1345 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1346 keyset_source, which controls if the current set of keybindings
1347 are from a builtin or custom keyset. DynOptionMenus builtinlist
1348 and customlist contain lists of the builtin and custom keysets,
1349 respectively, and the current item from each list is stored in
1350 vars builtin_name and custom_name.
1351
1352 Button delete_custom_keys invokes delete_custom_keys() to delete
1353 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1354 save_custom_keys invokes save_as_new_key_set() which calls
1355 get_new_keys_name() and create_new_key_set() to save a custom keyset
1356 and its keybindings to idleConf.userCfg['keys'].
1357
1358 Listbox bindingslist contains all of the keybindings for the
1359 selected keyset. The keybindings are loaded in load_keys_list()
1360 and are pairs of (event, [keys]) where keys can be a list
1361 of one or more key combinations to bind to the same event.
1362 Mouse button 1 click invokes on_bindingslist_select(), which
1363 allows button_new_keys to be clicked.
1364
1365 So, an item is selected in listbindings, which activates
1366 button_new_keys, and clicking button_new_keys calls function
1367 get_new_keys(). Function get_new_keys() gets the key mappings from the
1368 current keyset for the binding event item that was selected. The
1369 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001370 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001371 to be entered for that binding event. If the keys aren't
1372 changed, nothing happens. If the keys are changed and the keyset
1373 is a builtin, function get_new_keys_name() will be called
1374 for input of a custom keyset name. If no name is given, then the
1375 change to the keybinding will abort and no updates will be made. If
1376 a custom name is entered in the prompt or if the current keyset was
1377 already custom (and thus didn't require a prompt), then
1378 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1379 with the change to the event binding. The item listing in bindingslist
1380 is updated with the new keys. Var keybinding is also set which invokes
1381 the callback function, var_changed_keybinding, to add the change to
1382 the 'keys' or 'extensions' changes tracker based on the binding type.
1383
1384 Tk Variables:
1385 keybinding: Action/key bindings.
1386
1387 Methods:
1388 load_keys_list: Reload active set.
1389 create_new_key_set: Combine active keyset and changes.
1390 set_keys_type: Command for keyset_source.
1391 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1392 deactivate_current_config: Remove keys bindings in editors.
1393
1394 Widgets for KeysPage(frame): (*) widgets bound to self
1395 frame_key_sets: LabelFrame
1396 frames[0]: Frame
1397 (*)builtin_keyset_on: Radiobutton - var keyset_source
1398 (*)custom_keyset_on: Radiobutton - var keyset_source
1399 (*)builtinlist: DynOptionMenu - var builtin_name,
1400 func keybinding_selected
1401 (*)customlist: DynOptionMenu - var custom_name,
1402 func keybinding_selected
1403 (*)keys_message: Label
1404 frames[1]: Frame
1405 (*)button_delete_custom_keys: Button - delete_custom_keys
1406 (*)button_save_custom_keys: Button - save_as_new_key_set
1407 frame_custom: LabelFrame
1408 frame_target: Frame
1409 target_title: Label
1410 scroll_target_y: Scrollbar
1411 scroll_target_x: Scrollbar
1412 (*)bindingslist: ListBox - on_bindingslist_select
1413 (*)button_new_keys: Button - get_new_keys & ..._name
1414 """
1415 self.builtin_name = tracers.add(
1416 StringVar(self), self.var_changed_builtin_name)
1417 self.custom_name = tracers.add(
1418 StringVar(self), self.var_changed_custom_name)
1419 self.keyset_source = tracers.add(
1420 BooleanVar(self), self.var_changed_keyset_source)
1421 self.keybinding = tracers.add(
1422 StringVar(self), self.var_changed_keybinding)
1423
1424 # Create widgets:
1425 # body and section frames.
1426 frame_custom = LabelFrame(
1427 self, borderwidth=2, relief=GROOVE,
1428 text=' Custom Key Bindings ')
1429 frame_key_sets = LabelFrame(
1430 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1431 # frame_custom.
1432 frame_target = Frame(frame_custom)
1433 target_title = Label(frame_target, text='Action - Key(s)')
1434 scroll_target_y = Scrollbar(frame_target)
1435 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1436 self.bindingslist = Listbox(
1437 frame_target, takefocus=FALSE, exportselection=FALSE)
1438 self.bindingslist.bind('<ButtonRelease-1>',
1439 self.on_bindingslist_select)
1440 scroll_target_y['command'] = self.bindingslist.yview
1441 scroll_target_x['command'] = self.bindingslist.xview
1442 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1443 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1444 self.button_new_keys = Button(
1445 frame_custom, text='Get New Keys for Selection',
1446 command=self.get_new_keys, state=DISABLED)
1447 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001448 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001449 for i in range(2)]
1450 self.builtin_keyset_on = Radiobutton(
1451 frames[0], variable=self.keyset_source, value=1,
1452 command=self.set_keys_type, text='Use a Built-in Key Set')
1453 self.custom_keyset_on = Radiobutton(
1454 frames[0], variable=self.keyset_source, value=0,
1455 command=self.set_keys_type, text='Use a Custom Key Set')
1456 self.builtinlist = DynOptionMenu(
1457 frames[0], self.builtin_name, None, command=None)
1458 self.customlist = DynOptionMenu(
1459 frames[0], self.custom_name, None, command=None)
1460 self.button_delete_custom_keys = Button(
1461 frames[1], text='Delete Custom Key Set',
1462 command=self.delete_custom_keys)
1463 self.button_save_custom_keys = Button(
1464 frames[1], text='Save as New Custom Key Set',
1465 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001466 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001467
1468 # Pack widgets:
1469 # body.
1470 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1471 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1472 # frame_custom.
1473 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1474 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1475 # frame_target.
1476 frame_target.columnconfigure(0, weight=1)
1477 frame_target.rowconfigure(1, weight=1)
1478 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1479 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1480 scroll_target_y.grid(row=1, column=1, sticky=NS)
1481 scroll_target_x.grid(row=2, column=0, sticky=EW)
1482 # frame_key_sets.
1483 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1484 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1485 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1486 self.customlist.grid(row=1, column=1, sticky=NSEW)
1487 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1488 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1489 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1490 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1491 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1492
1493 def load_key_cfg(self):
1494 "Load current configuration settings for the keybinding options."
1495 # Set current keys type radiobutton.
1496 self.keyset_source.set(idleConf.GetOption(
1497 'main', 'Keys', 'default', type='bool', default=1))
1498 # Set current keys.
1499 current_option = idleConf.CurrentKeys()
1500 # Load available keyset option menus.
1501 if self.keyset_source.get(): # Default theme selected.
1502 item_list = idleConf.GetSectionList('default', 'keys')
1503 item_list.sort()
1504 self.builtinlist.SetMenu(item_list, current_option)
1505 item_list = idleConf.GetSectionList('user', 'keys')
1506 item_list.sort()
1507 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001508 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001509 self.custom_name.set('- no custom keys -')
1510 else:
1511 self.customlist.SetMenu(item_list, item_list[0])
1512 else: # User key set selected.
1513 item_list = idleConf.GetSectionList('user', 'keys')
1514 item_list.sort()
1515 self.customlist.SetMenu(item_list, current_option)
1516 item_list = idleConf.GetSectionList('default', 'keys')
1517 item_list.sort()
1518 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1519 self.set_keys_type()
1520 # Load keyset element list.
1521 keyset_name = idleConf.CurrentKeys()
1522 self.load_keys_list(keyset_name)
1523
1524 def var_changed_builtin_name(self, *params):
1525 "Process selection of builtin key set."
1526 old_keys = (
1527 'IDLE Classic Windows',
1528 'IDLE Classic Unix',
1529 'IDLE Classic Mac',
1530 'IDLE Classic OSX',
1531 )
1532 value = self.builtin_name.get()
1533 if value not in old_keys:
1534 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1535 changes.add_option('main', 'Keys', 'name', old_keys[0])
1536 changes.add_option('main', 'Keys', 'name2', value)
1537 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001538 else:
1539 changes.add_option('main', 'Keys', 'name', value)
1540 changes.add_option('main', 'Keys', 'name2', '')
1541 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001542 self.load_keys_list(value)
1543
1544 def var_changed_custom_name(self, *params):
1545 "Process selection of custom key set."
1546 value = self.custom_name.get()
1547 if value != '- no custom keys -':
1548 changes.add_option('main', 'Keys', 'name', value)
1549 self.load_keys_list(value)
1550
1551 def var_changed_keyset_source(self, *params):
1552 "Process toggle between builtin key set and custom key set."
1553 value = self.keyset_source.get()
1554 changes.add_option('main', 'Keys', 'default', value)
1555 if value:
1556 self.var_changed_builtin_name()
1557 else:
1558 self.var_changed_custom_name()
1559
1560 def var_changed_keybinding(self, *params):
1561 "Store change to a keybinding."
1562 value = self.keybinding.get()
1563 key_set = self.custom_name.get()
1564 event = self.bindingslist.get(ANCHOR).split()[0]
1565 if idleConf.IsCoreBinding(event):
1566 changes.add_option('keys', key_set, event, value)
1567 else: # Event is an extension binding.
1568 ext_name = idleConf.GetExtnNameForEvent(event)
1569 ext_keybind_section = ext_name + '_cfgBindings'
1570 changes.add_option('extensions', ext_keybind_section, event, value)
1571
1572 def set_keys_type(self):
1573 "Set available screen options based on builtin or custom key set."
1574 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001575 self.builtinlist['state'] = 'normal'
1576 self.customlist['state'] = 'disabled'
1577 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001578 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001579 self.builtinlist['state'] = 'disabled'
1580 self.custom_keyset_on.state(('!disabled',))
1581 self.customlist['state'] = 'normal'
1582 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001583
1584 def get_new_keys(self):
1585 """Handle event to change key binding for selected line.
1586
1587 A selection of a key/binding in the list of current
1588 bindings pops up a dialog to enter a new binding. If
1589 the current key set is builtin and a binding has
1590 changed, then a name for a custom key set needs to be
1591 entered for the change to be applied.
1592 """
1593 list_index = self.bindingslist.index(ANCHOR)
1594 binding = self.bindingslist.get(list_index)
1595 bind_name = binding.split()[0]
1596 if self.keyset_source.get():
1597 current_key_set_name = self.builtin_name.get()
1598 else:
1599 current_key_set_name = self.custom_name.get()
1600 current_bindings = idleConf.GetCurrentKeySet()
1601 if current_key_set_name in changes['keys']: # unsaved changes
1602 key_set_changes = changes['keys'][current_key_set_name]
1603 for event in key_set_changes:
1604 current_bindings[event] = key_set_changes[event].split()
1605 current_key_sequences = list(current_bindings.values())
1606 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1607 current_key_sequences).result
1608 if new_keys:
1609 if self.keyset_source.get(): # Current key set is a built-in.
1610 message = ('Your changes will be saved as a new Custom Key Set.'
1611 ' Enter a name for your new Custom Key Set below.')
1612 new_keyset = self.get_new_keys_name(message)
1613 if not new_keyset: # User cancelled custom key set creation.
1614 self.bindingslist.select_set(list_index)
1615 self.bindingslist.select_anchor(list_index)
1616 return
1617 else: # Create new custom key set based on previously active key set.
1618 self.create_new_key_set(new_keyset)
1619 self.bindingslist.delete(list_index)
1620 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1621 self.bindingslist.select_set(list_index)
1622 self.bindingslist.select_anchor(list_index)
1623 self.keybinding.set(new_keys)
1624 else:
1625 self.bindingslist.select_set(list_index)
1626 self.bindingslist.select_anchor(list_index)
1627
1628 def get_new_keys_name(self, message):
1629 "Return new key set name from query popup."
1630 used_names = (idleConf.GetSectionList('user', 'keys') +
1631 idleConf.GetSectionList('default', 'keys'))
1632 new_keyset = SectionName(
1633 self, 'New Custom Key Set', message, used_names).result
1634 return new_keyset
1635
1636 def save_as_new_key_set(self):
1637 "Prompt for name of new key set and save changes using that name."
1638 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1639 if new_keys_name:
1640 self.create_new_key_set(new_keys_name)
1641
1642 def on_bindingslist_select(self, event):
1643 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001644 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001645
1646 def create_new_key_set(self, new_key_set_name):
1647 """Create a new custom key set with the given name.
1648
1649 Copy the bindings/keys from the previously active keyset
1650 to the new keyset and activate the new custom keyset.
1651 """
1652 if self.keyset_source.get():
1653 prev_key_set_name = self.builtin_name.get()
1654 else:
1655 prev_key_set_name = self.custom_name.get()
1656 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1657 new_keys = {}
1658 for event in prev_keys: # Add key set to changed items.
1659 event_name = event[2:-2] # Trim off the angle brackets.
1660 binding = ' '.join(prev_keys[event])
1661 new_keys[event_name] = binding
1662 # Handle any unsaved changes to prev key set.
1663 if prev_key_set_name in changes['keys']:
1664 key_set_changes = changes['keys'][prev_key_set_name]
1665 for event in key_set_changes:
1666 new_keys[event] = key_set_changes[event]
1667 # Save the new key set.
1668 self.save_new_key_set(new_key_set_name, new_keys)
1669 # Change GUI over to the new key set.
1670 custom_key_list = idleConf.GetSectionList('user', 'keys')
1671 custom_key_list.sort()
1672 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1673 self.keyset_source.set(0)
1674 self.set_keys_type()
1675
1676 def load_keys_list(self, keyset_name):
1677 """Reload the list of action/key binding pairs for the active key set.
1678
1679 An action/key binding can be selected to change the key binding.
1680 """
1681 reselect = False
1682 if self.bindingslist.curselection():
1683 reselect = True
1684 list_index = self.bindingslist.index(ANCHOR)
1685 keyset = idleConf.GetKeySet(keyset_name)
1686 bind_names = list(keyset.keys())
1687 bind_names.sort()
1688 self.bindingslist.delete(0, END)
1689 for bind_name in bind_names:
1690 key = ' '.join(keyset[bind_name])
1691 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1692 if keyset_name in changes['keys']:
1693 # Handle any unsaved changes to this key set.
1694 if bind_name in changes['keys'][keyset_name]:
1695 key = changes['keys'][keyset_name][bind_name]
1696 self.bindingslist.insert(END, bind_name+' - '+key)
1697 if reselect:
1698 self.bindingslist.see(list_index)
1699 self.bindingslist.select_set(list_index)
1700 self.bindingslist.select_anchor(list_index)
1701
1702 @staticmethod
1703 def save_new_key_set(keyset_name, keyset):
1704 """Save a newly created core key set.
1705
1706 Add keyset to idleConf.userCfg['keys'], not to disk.
1707 If the keyset doesn't exist, it is created. The
1708 binding/keys are taken from the keyset argument.
1709
1710 keyset_name - string, the name of the new key set
1711 keyset - dictionary containing the new keybindings
1712 """
1713 if not idleConf.userCfg['keys'].has_section(keyset_name):
1714 idleConf.userCfg['keys'].add_section(keyset_name)
1715 for event in keyset:
1716 value = keyset[event]
1717 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1718
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001719 def askyesno(self, *args, **kwargs):
1720 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001721 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001722
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001723 def delete_custom_keys(self):
1724 """Handle event to delete a custom key set.
1725
1726 Applying the delete deactivates the current configuration and
1727 reverts to the default. The custom key set is permanently
1728 deleted from the config file.
1729 """
1730 keyset_name = self.custom_name.get()
1731 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001732 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001733 'Delete Key Set', delmsg % keyset_name, parent=self):
1734 return
1735 self.cd.deactivate_current_config()
1736 # Remove key set from changes, config, and file.
1737 changes.delete_section('keys', keyset_name)
1738 # Reload user key set list.
1739 item_list = idleConf.GetSectionList('user', 'keys')
1740 item_list.sort()
1741 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001742 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001743 self.customlist.SetMenu(item_list, '- no custom keys -')
1744 else:
1745 self.customlist.SetMenu(item_list, item_list[0])
1746 # Revert to default key set.
1747 self.keyset_source.set(idleConf.defaultCfg['main']
1748 .Get('Keys', 'default'))
1749 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1750 or idleConf.default_keys())
1751 # User can't back out of these changes, they must be applied now.
1752 changes.save_all()
1753 self.cd.save_all_changed_extensions()
1754 self.cd.activate_config_changes()
1755 self.set_keys_type()
1756
1757
csabellae8eb17b2017-07-30 18:39:17 -04001758class GenPage(Frame):
1759
csabella6f446be2017-08-01 00:24:07 -04001760 def __init__(self, master):
1761 super().__init__(master)
csabellae8eb17b2017-07-30 18:39:17 -04001762 self.create_page_general()
1763 self.load_general_cfg()
1764
1765 def create_page_general(self):
1766 """Return frame of widgets for General tab.
1767
1768 Enable users to provisionally change general options. Function
1769 load_general_cfg intializes tk variables and helplist using
1770 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1771 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1772 set var autosave. Entry boxes win_width_int and win_height_int
1773 set var win_width and win_height. Setting var_name invokes the
1774 default callback that adds option to changes.
1775
1776 Helplist: load_general_cfg loads list user_helplist with
1777 name, position pairs and copies names to listbox helplist.
1778 Clicking a name invokes help_source selected. Clicking
1779 button_helplist_name invokes helplist_item_name, which also
1780 changes user_helplist. These functions all call
1781 set_add_delete_state. All but load call update_help_changes to
1782 rewrite changes['main']['HelpFiles'].
1783
Cheryl Sabella2f896462017-08-14 21:21:43 -04001784 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001785 frame_window: LabelFrame
1786 frame_run: Frame
1787 startup_title: Label
1788 (*)startup_editor_on: Radiobutton - startup_edit
1789 (*)startup_shell_on: Radiobutton - startup_edit
1790 frame_win_size: Frame
1791 win_size_title: Label
1792 win_width_title: Label
1793 (*)win_width_int: Entry - win_width
1794 win_height_title: Label
1795 (*)win_height_int: Entry - win_height
1796 frame_editor: LabelFrame
1797 frame_save: Frame
1798 run_save_title: Label
1799 (*)save_ask_on: Radiobutton - autosave
1800 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella2f896462017-08-14 21:21:43 -04001801 frame_help: LabelFrame
1802 frame_helplist: Frame
1803 frame_helplist_buttons: Frame
1804 (*)button_helplist_edit
1805 (*)button_helplist_add
1806 (*)button_helplist_remove
1807 (*)helplist: ListBox
1808 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001809 """
wohlganger58fc71c2017-09-10 16:19:47 -05001810 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001811 self.startup_edit = tracers.add(
1812 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001813 self.win_width = tracers.add(
1814 StringVar(self), ('main', 'EditorWindow', 'width'))
1815 self.win_height = tracers.add(
1816 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001817 self.autocomplete_wait = tracers.add(
1818 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1819 self.paren_style = tracers.add(
1820 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1821 self.flash_delay = tracers.add(
1822 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1823 self.paren_bell = tracers.add(
1824 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001825
wohlganger58fc71c2017-09-10 16:19:47 -05001826 self.autosave = tracers.add(
1827 IntVar(self), ('main', 'General', 'autosave'))
1828 self.format_width = tracers.add(
1829 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
1830 self.context_lines = tracers.add(
1831 StringVar(self), ('extensions', 'CodeContext', 'numlines'))
1832
1833 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001834 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001835 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1836 text=' Window Preferences')
1837 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1838 text=' Editor Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001839 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1840 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001841 # Frame_window.
1842 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001843 startup_title = Label(frame_run, text='At Startup')
1844 self.startup_editor_on = Radiobutton(
1845 frame_run, variable=self.startup_edit, value=1,
1846 text="Open Edit Window")
1847 self.startup_shell_on = Radiobutton(
1848 frame_run, variable=self.startup_edit, value=0,
1849 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001850
wohlganger58fc71c2017-09-10 16:19:47 -05001851 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001852 win_size_title = Label(
1853 frame_win_size, text='Initial Window Size (in characters)')
1854 win_width_title = Label(frame_win_size, text='Width')
1855 self.win_width_int = Entry(
1856 frame_win_size, textvariable=self.win_width, width=3)
1857 win_height_title = Label(frame_win_size, text='Height')
1858 self.win_height_int = Entry(
1859 frame_win_size, textvariable=self.win_height, width=3)
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001860
wohlganger58fc71c2017-09-10 16:19:47 -05001861 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1862 auto_wait_title = Label(frame_autocomplete,
1863 text='Completions Popup Wait (milliseconds)')
1864 self.auto_wait_int = Entry(frame_autocomplete, width=6,
1865 textvariable=self.autocomplete_wait)
1866
1867 frame_paren1 = Frame(frame_window, borderwidth=0)
1868 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1869 self.paren_style_type = OptionMenu(
1870 frame_paren1, self.paren_style, 'expression',
1871 "opener","parens","expression")
1872 frame_paren2 = Frame(frame_window, borderwidth=0)
1873 paren_time_title = Label(
1874 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1875 '(0 is until next input)')
1876 self.paren_flash_time = Entry(
1877 frame_paren2, textvariable=self.flash_delay, width=6)
1878 self.bell_on = Checkbutton(
1879 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1880
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001881 # Frame_editor.
1882 frame_save = Frame(frame_editor, borderwidth=0)
1883 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1884 self.save_ask_on = Radiobutton(
1885 frame_save, variable=self.autosave, value=0,
1886 text="Prompt to Save")
1887 self.save_auto_on = Radiobutton(
1888 frame_save, variable=self.autosave, value=1,
1889 text='No Prompt')
1890
wohlganger58fc71c2017-09-10 16:19:47 -05001891 frame_format = Frame(frame_editor, borderwidth=0)
1892 format_width_title = Label(frame_format,
1893 text='Format Paragraph Max Width')
1894 self.format_width_int = Entry(
1895 frame_format, textvariable=self.format_width, width=4)
1896
1897 frame_context = Frame(frame_editor, borderwidth=0)
1898 context_title = Label(frame_context, text='Context Lines :')
1899 self.context_int = Entry(
1900 frame_context, textvariable=self.context_lines, width=3)
1901
1902
csabellae8eb17b2017-07-30 18:39:17 -04001903 # frame_help.
1904 frame_helplist = Frame(frame_help)
1905 frame_helplist_buttons = Frame(frame_helplist)
1906 self.helplist = Listbox(
1907 frame_helplist, height=5, takefocus=True,
1908 exportselection=FALSE)
1909 scroll_helplist = Scrollbar(frame_helplist)
1910 scroll_helplist['command'] = self.helplist.yview
1911 self.helplist['yscrollcommand'] = scroll_helplist.set
1912 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1913 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001914 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001915 width=8, command=self.helplist_item_edit)
1916 self.button_helplist_add = Button(
1917 frame_helplist_buttons, text='Add',
1918 width=8, command=self.helplist_item_add)
1919 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001920 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001921 width=8, command=self.helplist_item_remove)
1922
1923 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001924 # Body.
1925 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1926 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04001927 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1928 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001929 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001930 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1931 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1932 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04001933 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001934 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001935 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1936 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1937 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1938 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1939 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001940 # frame_autocomplete.
1941 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
1942 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1943 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
1944 # frame_paren.
1945 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
1946 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1947 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
1948 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
1949 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
1950 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1951 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
1952
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001953 # frame_save.
1954 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
1955 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1956 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1957 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001958 # frame_format.
1959 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
1960 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1961 self.format_width_int.pack(side=TOP, padx=10, pady=5)
1962 # frame_context.
1963 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
1964 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1965 self.context_int.pack(side=TOP, padx=5, pady=5)
1966
csabellae8eb17b2017-07-30 18:39:17 -04001967 # frame_help.
1968 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1969 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1970 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1971 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1972 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1973 self.button_helplist_add.pack(side=TOP, anchor=W)
1974 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1975
1976 def load_general_cfg(self):
1977 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05001978 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04001979 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05001980 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04001981 self.win_width.set(idleConf.GetOption(
1982 'main', 'EditorWindow', 'width', type='int'))
1983 self.win_height.set(idleConf.GetOption(
1984 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05001985 self.autocomplete_wait.set(idleConf.GetOption(
1986 'extensions', 'AutoComplete', 'popupwait', type='int'))
1987 self.paren_style.set(idleConf.GetOption(
1988 'extensions', 'ParenMatch', 'style'))
1989 self.flash_delay.set(idleConf.GetOption(
1990 'extensions', 'ParenMatch', 'flash-delay', type='int'))
1991 self.paren_bell.set(idleConf.GetOption(
1992 'extensions', 'ParenMatch', 'bell'))
1993
1994 # Set variables for editor windows.
1995 self.autosave.set(idleConf.GetOption(
1996 'main', 'General', 'autosave', default=0, type='bool'))
1997 self.format_width.set(idleConf.GetOption(
1998 'extensions', 'FormatParagraph', 'max-width', type='int'))
1999 self.context_lines.set(idleConf.GetOption(
2000 'extensions', 'CodeContext', 'numlines', type='int'))
2001
csabellae8eb17b2017-07-30 18:39:17 -04002002 # Set additional help sources.
2003 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2004 self.helplist.delete(0, 'end')
2005 for help_item in self.user_helplist:
2006 self.helplist.insert(END, help_item[0])
2007 self.set_add_delete_state()
2008
2009 def help_source_selected(self, event):
2010 "Handle event for selecting additional help."
2011 self.set_add_delete_state()
2012
2013 def set_add_delete_state(self):
2014 "Toggle the state for the help list buttons based on list entries."
2015 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002016 self.button_helplist_edit.state(('disabled',))
2017 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002018 else: # Some entries.
2019 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002020 self.button_helplist_edit.state(('!disabled',))
2021 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002022 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002023 self.button_helplist_edit.state(('disabled',))
2024 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002025
2026 def helplist_item_add(self):
2027 """Handle add button for the help list.
2028
2029 Query for name and location of new help sources and add
2030 them to the list.
2031 """
2032 help_source = HelpSource(self, 'New Help Source').result
2033 if help_source:
2034 self.user_helplist.append(help_source)
2035 self.helplist.insert(END, help_source[0])
2036 self.update_help_changes()
2037
2038 def helplist_item_edit(self):
2039 """Handle edit button for the help list.
2040
2041 Query with existing help source information and update
2042 config if the values are changed.
2043 """
2044 item_index = self.helplist.index(ANCHOR)
2045 help_source = self.user_helplist[item_index]
2046 new_help_source = HelpSource(
2047 self, 'Edit Help Source',
2048 menuitem=help_source[0],
2049 filepath=help_source[1],
2050 ).result
2051 if new_help_source and new_help_source != help_source:
2052 self.user_helplist[item_index] = new_help_source
2053 self.helplist.delete(item_index)
2054 self.helplist.insert(item_index, new_help_source[0])
2055 self.update_help_changes()
2056 self.set_add_delete_state() # Selected will be un-selected
2057
2058 def helplist_item_remove(self):
2059 """Handle remove button for the help list.
2060
2061 Delete the help list item from config.
2062 """
2063 item_index = self.helplist.index(ANCHOR)
2064 del(self.user_helplist[item_index])
2065 self.helplist.delete(item_index)
2066 self.update_help_changes()
2067 self.set_add_delete_state()
2068
2069 def update_help_changes(self):
2070 "Clear and rebuild the HelpFiles section in changes"
2071 changes['main']['HelpFiles'] = {}
2072 for num in range(1, len(self.user_helplist) + 1):
2073 changes.add_option(
2074 'main', 'HelpFiles', str(num),
2075 ';'.join(self.user_helplist[num-1][:2]))
2076
2077
csabella45bf7232017-07-26 19:09:58 -04002078class VarTrace:
2079 """Maintain Tk variables trace state."""
2080
2081 def __init__(self):
2082 """Store Tk variables and callbacks.
2083
2084 untraced: List of tuples (var, callback)
2085 that do not have the callback attached
2086 to the Tk var.
2087 traced: List of tuples (var, callback) where
2088 that callback has been attached to the var.
2089 """
2090 self.untraced = []
2091 self.traced = []
2092
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002093 def clear(self):
2094 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002095 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002096 self.untraced.clear()
2097 self.traced.clear()
2098
csabella45bf7232017-07-26 19:09:58 -04002099 def add(self, var, callback):
2100 """Add (var, callback) tuple to untraced list.
2101
2102 Args:
2103 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002104 callback: Either function name to be used as a callback
2105 or a tuple with IdleConf config-type, section, and
2106 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002107
2108 Return:
2109 Tk variable instance.
2110 """
2111 if isinstance(callback, tuple):
2112 callback = self.make_callback(var, callback)
2113 self.untraced.append((var, callback))
2114 return var
2115
2116 @staticmethod
2117 def make_callback(var, config):
2118 "Return default callback function to add values to changes instance."
2119 def default_callback(*params):
2120 "Add config values to changes instance."
2121 changes.add_option(*config, var.get())
2122 return default_callback
2123
2124 def attach(self):
2125 "Attach callback to all vars that are not traced."
2126 while self.untraced:
2127 var, callback = self.untraced.pop()
2128 var.trace_add('write', callback)
2129 self.traced.append((var, callback))
2130
2131 def detach(self):
2132 "Remove callback from traced vars."
2133 while self.traced:
2134 var, callback = self.traced.pop()
2135 var.trace_remove('write', var.trace_info()[0][1])
2136 self.untraced.append((var, callback))
2137
2138
csabella5b591542017-07-28 14:40:59 -04002139tracers = VarTrace()
2140
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002141help_common = '''\
2142When you click either the Apply or Ok buttons, settings in this
2143dialog that are different from IDLE's default are saved in
2144a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002145these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002146machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002147'''
2148help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002149 'Fonts/Tabs':'''
2150Font sample: This shows what a selection of Basic Multilingual Plane
2151unicode characters look like for the current font selection. If the
2152selected font does not define a character, Tk attempts to find another
2153font that does. Substitute glyphs depend on what is available on a
2154particular system and will not necessarily have the same size as the
2155font selected. Line contains 20 characters up to Devanagari, 14 for
2156Tamil, and 10 for East Asia.
2157
2158Hebrew and Arabic letters should display right to left, starting with
2159alef, \u05d0 and \u0627. Arabic digits display left to right. The
2160Devanagari and Tamil lines start with digits. The East Asian lines
2161are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2162Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002163
2164You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002165''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002166 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002167Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002168The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002169be used with older IDLE releases if it is saved as a custom
2170theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002171''',
2172 'Keys': '''
2173Keys:
2174The IDLE Modern Unix key set is new in June 2016. It can only
2175be used with older IDLE releases if it is saved as a custom
2176key set, with a different name.
2177''',
wohlganger58fc71c2017-09-10 16:19:47 -05002178 'General': '''
2179General:
wohlgangerfae2c352017-06-27 21:36:23 -05002180
wohlganger58fc71c2017-09-10 16:19:47 -05002181AutoComplete: Popupwait is milleseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002182cursor movement, before popping up completion box. Key char is '.' after
2183identifier or a '/' (or '\\' on Windows) within a string.
2184
2185FormatParagraph: Max-width is max chars in lines after re-formatting.
2186Use with paragraphs in both strings and comment blocks.
2187
2188ParenMatch: Style indicates what is highlighted when closer is entered:
2189'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2190'expression' (default) - also everything in between. Flash-delay is how
2191long to highlight if cursor is not moved (0 means forever).
2192'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002193}
2194
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002195
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002196def is_int(s):
2197 "Return 's is blank or represents an int'"
2198 if not s:
2199 return True
2200 try:
2201 int(s)
2202 return True
2203 except ValueError:
2204 return False
2205
2206
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002207class VerticalScrolledFrame(Frame):
2208 """A pure Tkinter vertically scrollable frame.
2209
2210 * Use the 'interior' attribute to place widgets inside the scrollable frame
2211 * Construct and pack/place/grid normally
2212 * This frame only allows vertical scrolling
2213 """
2214 def __init__(self, parent, *args, **kw):
2215 Frame.__init__(self, parent, *args, **kw)
2216
csabella7eb58832017-07-04 21:30:58 -04002217 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002218 vscrollbar = Scrollbar(self, orient=VERTICAL)
2219 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002220 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002221 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002222 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2223 vscrollbar.config(command=canvas.yview)
2224
csabella7eb58832017-07-04 21:30:58 -04002225 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002226 canvas.xview_moveto(0)
2227 canvas.yview_moveto(0)
2228
csabella7eb58832017-07-04 21:30:58 -04002229 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002230 self.interior = interior = Frame(canvas)
2231 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2232
csabella7eb58832017-07-04 21:30:58 -04002233 # Track changes to the canvas and frame width and sync them,
2234 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002235 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002236 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002237 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2238 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002239 interior.bind('<Configure>', _configure_interior)
2240
2241 def _configure_canvas(event):
2242 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002243 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002244 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2245 canvas.bind('<Configure>', _configure_canvas)
2246
2247 return
2248
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002249
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002250if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002251 import unittest
2252 unittest.main('idlelib.idle_test.test_configdialog',
2253 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002254 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002255 run(ConfigDialog)