blob: e682ec0da3200eb51895810fe86b04e3ad469cdf [file] [log] [blame]
Kurt B. Kaisere7a161e2003-01-10 20:13:57 +00001"""IDLE Configuration Dialog: support user customization of IDLE by GUI
2
3Customize font faces, sizes, and colorization attributes. Set indentation
4defaults. Customize keybindings. Colorization and keybindings can be
5saved as user defined sets. Select startup options including shell/editor
6and default window size. Define additional help sources.
7
8Note that tab width in IDLE is currently fixed at eight due to Tk issues.
Kurt B. Kaiseracdef852005-01-31 03:34:26 +00009Refer to comments in EditorWindow autoindent code for details.
Kurt B. Kaisere7a161e2003-01-10 20:13:57 +000010
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000011"""
Cheryl Sabella7028e592017-08-26 14:26:02 -040012from tkinter import (Toplevel, Listbox, Text, Scale, Canvas,
csabellabac7d332017-06-26 17:46:26 -040013 StringVar, BooleanVar, IntVar, TRUE, FALSE,
Terry Jan Reedye8f7c782017-11-28 21:52:32 -050014 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE,
15 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
Louie Lubb2bae82017-07-10 06:57:18 +080016 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
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 Reedy6fa5bdc2016-05-28 13:22:31 -040028from idlelib.textview import view_text
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040029from idlelib.autocomplete import AutoComplete
30from idlelib.codecontext import CodeContext
31from idlelib.parenmatch import ParenMatch
32from idlelib.paragraph import FormatParagraph
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -040033
terryjreedy349abd92017-07-07 16:00:57 -040034changes = ConfigChanges()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040035# Reload changed options in the following classes.
36reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph)
terryjreedy349abd92017-07-07 16:00:57 -040037
csabella5b591542017-07-28 14:40:59 -040038
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000039class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -040040 """Config dialog for IDLE.
41 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000042
Terry Jan Reedybfebfd82017-09-30 17:37:53 -040043 def __init__(self, parent, title='', *, _htest=False, _utest=False):
csabella7eb58832017-07-04 21:30:58 -040044 """Show the tabbed dialog for user configuration.
45
csabella36329a42017-07-13 23:32:01 -040046 Args:
47 parent - parent of this dialog
48 title - string which is the title of this popup dialog
49 _htest - bool, change box location when running htest
50 _utest - bool, don't wait_window when running unittest
51
52 Note: Focus set on font page fontlist.
53
54 Methods:
55 create_widgets
56 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040057 """
Steven M. Gavad721c482001-07-31 10:46:53 +000058 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040059 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040060 if _htest:
61 parent.instance_dict = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080062 if not _utest:
63 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000064
Steven M. Gavad721c482001-07-31 10:46:53 +000065 self.configure(borderwidth=5)
Terry Jan Reedycd567362014-10-17 01:31:35 -040066 self.title(title or 'IDLE Preferences')
csabellabac7d332017-06-26 17:46:26 -040067 x = parent.winfo_rootx() + 20
68 y = parent.winfo_rooty() + (30 if not _htest else 150)
69 self.geometry(f'+{x}+{y}')
csabella7eb58832017-07-04 21:30:58 -040070 # Each theme element key is its display name.
71 # The first value of the tuple is the sample area tag name.
72 # The second value is the display name list sort index.
csabellabac7d332017-06-26 17:46:26 -040073 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040074 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000075 self.transient(parent)
csabellabac7d332017-06-26 17:46:26 -040076 self.protocol("WM_DELETE_WINDOW", self.cancel)
csabella9397e2a2017-07-30 13:34:25 -040077 self.fontpage.fontlist.focus_set()
csabella7eb58832017-07-04 21:30:58 -040078 # XXX Decide whether to keep or delete these key bindings.
79 # Key bindings for this dialog.
80 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
81 # self.bind('<Alt-a>', self.Apply) #apply changes, save
82 # self.bind('<F1>', self.Help) #context help
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040083 # Attach callbacks after loading config to avoid calling them.
csabella5b591542017-07-28 14:40:59 -040084 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000085
Terry Jan Reedycfa89502014-07-14 23:07:32 -040086 if not _utest:
Louie Lu9b622fb2017-07-14 08:35:48 +080087 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040088 self.wm_deiconify()
89 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000090
csabellabac7d332017-06-26 17:46:26 -040091 def create_widgets(self):
csabella36329a42017-07-13 23:32:01 -040092 """Create and place widgets for tabbed dialog.
93
94 Widgets Bound to self:
csabellae8eb17b2017-07-30 18:39:17 -040095 note: Notebook
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040096 highpage: HighPage
csabellae8eb17b2017-07-30 18:39:17 -040097 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -040098 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -040099 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400100 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -0400101
102 Methods:
csabella36329a42017-07-13 23:32:01 -0400103 create_action_buttons
104 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400105 activate_config_changes: Tell editors to reload.
106 """
Terry Jan Reedyd6e2f262017-09-19 19:01:45 -0400107 self.note = note = Notebook(self)
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400108 self.highpage = HighPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400109 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400110 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400111 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400112 self.extpage = self.create_page_extensions()
113 note.add(self.fontpage, text='Fonts/Tabs')
114 note.add(self.highpage, text='Highlights')
115 note.add(self.keyspage, text=' Keys ')
116 note.add(self.genpage, text=' General ')
117 note.add(self.extpage, text='Extensions')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400118 note.enable_traversal()
119 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400120 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400121
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400122 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -0400123 """Return frame of action buttons for dialog.
124
125 Methods:
126 ok
127 apply
128 cancel
129 help
130
131 Widget Structure:
132 outer: Frame
133 buttons: Frame
134 (no assignment): Button (ok)
135 (no assignment): Button (apply)
136 (no assignment): Button (cancel)
137 (no assignment): Button (help)
138 (no assignment): Frame
139 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400140 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400141 # Changing the default padding on OSX results in unreadable
csabella7eb58832017-07-04 21:30:58 -0400142 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400143 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000144 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400145 padding_args = {'padding': (6, 3)}
146 outer = Frame(self, padding=2)
147 buttons = Frame(outer, padding=2)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400148 for txt, cmd in (
csabellabac7d332017-06-26 17:46:26 -0400149 ('Ok', self.ok),
150 ('Apply', self.apply),
151 ('Cancel', self.cancel),
152 ('Help', self.help)):
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400153 Button(buttons, text=txt, command=cmd, takefocus=FALSE,
csabellabac7d332017-06-26 17:46:26 -0400154 **padding_args).pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -0400155 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400156 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
157 buttons.pack(side=BOTTOM)
158 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400159
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400160 def ok(self):
161 """Apply config changes, then dismiss dialog.
162
163 Methods:
164 apply
165 destroy: inherited
166 """
167 self.apply()
168 self.destroy()
169
170 def apply(self):
171 """Apply config changes and leave dialog open.
172
173 Methods:
174 deactivate_current_config
175 save_all_changed_extensions
176 activate_config_changes
177 """
178 self.deactivate_current_config()
179 changes.save_all()
180 self.save_all_changed_extensions()
181 self.activate_config_changes()
182
183 def cancel(self):
184 """Dismiss config dialog.
185
186 Methods:
187 destroy: inherited
188 """
189 self.destroy()
190
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300191 def destroy(self):
192 global font_sample_text
193 font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
Tal Einat10ea9402018-08-02 09:18:29 +0300194 self.grab_release()
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300195 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',
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400377 validatecommand=(self.is_int, '%P'), width=10
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400378 ).grid(row=row, column=1, sticky=NSEW, padx=7)
379
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400380 else: # type == 'str'
381 # Limit size to fit non-expanding space with larger font.
382 Entry(entry_area, textvariable=var, width=15
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400383 ).grid(row=row, column=1, sticky=NSEW, padx=7)
384 return
385
386 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400387 """Return True if the configuration was added or changed.
388
389 If the value is the same as the default, then remove it
390 from user config file.
391 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400392 name = opt['name']
393 default = opt['default']
394 value = opt['var'].get().strip() or default
395 opt['var'].set(value)
396 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400397 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400398 if (value == default):
399 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400400 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400401 return self.ext_userCfg.SetOption(section, name, value)
402
403 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400404 """Save configuration changes to the user config file.
405
406 Attributes accessed:
407 extensions
408
409 Methods:
410 set_extension_value
411 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400412 has_changes = False
413 for ext_name in self.extensions:
414 options = self.extensions[ext_name]
415 for opt in options:
416 if self.set_extension_value(ext_name, opt):
417 has_changes = True
418 if has_changes:
419 self.ext_userCfg.Save()
420
421
csabella6f446be2017-08-01 00:24:07 -0400422# class TabPage(Frame): # A template for Page classes.
423# def __init__(self, master):
424# super().__init__(master)
425# self.create_page_tab()
426# self.load_tab_cfg()
427# def create_page_tab(self):
428# # Define tk vars and register var and callback with tracers.
429# # Create subframes and widgets.
430# # Pack widgets.
431# def load_tab_cfg(self):
432# # Initialize widgets with data from idleConf.
433# def var_changed_var_name():
434# # For each tk var that needs other than default callback.
435# def other_methods():
436# # Define tab-specific behavior.
437
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300438font_sample_text = (
439 '<ASCII/Latin1>\n'
440 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
441 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
442 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
443 '\n<IPA,Greek,Cyrillic>\n'
444 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
445 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
446 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
447 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
448 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
449 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
450 '\n<Hebrew, Arabic>\n'
451 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
452 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
453 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
454 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
455 '\n<Devanagari, Tamil>\n'
456 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
457 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
458 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
459 '\u0b85\u0b87\u0b89\u0b8e\n'
460 '\n<East Asian>\n'
461 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
462 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
463 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
464 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
465 )
466
csabella6f446be2017-08-01 00:24:07 -0400467
csabella9397e2a2017-07-30 13:34:25 -0400468class FontPage(Frame):
469
csabella6f446be2017-08-01 00:24:07 -0400470 def __init__(self, master, highpage):
471 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400472 self.highlight_sample = highpage.highlight_sample
473 self.create_page_font_tab()
474 self.load_font_cfg()
475 self.load_tab_cfg()
476
477 def create_page_font_tab(self):
478 """Return frame of widgets for Font/Tabs tab.
479
480 Fonts: Enable users to provisionally change font face, size, or
481 boldness and to see the consequence of proposed choices. Each
482 action set 3 options in changes structuree and changes the
483 corresponding aspect of the font sample on this page and
484 highlight sample on highlight page.
485
486 Function load_font_cfg initializes font vars and widgets from
487 idleConf entries and tk.
488
489 Fontlist: mouse button 1 click or up or down key invoke
490 on_fontlist_select(), which sets var font_name.
491
492 Sizelist: clicking the menubutton opens the dropdown menu. A
493 mouse button 1 click or return key sets var font_size.
494
495 Bold_toggle: clicking the box toggles var font_bold.
496
497 Changing any of the font vars invokes var_changed_font, which
498 adds all 3 font options to changes and calls set_samples.
499 Set_samples applies a new font constructed from the font vars to
Leo Ariasc3d95082018-02-03 18:36:10 -0600500 font_sample and to highlight_sample on the highlight page.
csabella9397e2a2017-07-30 13:34:25 -0400501
502 Tabs: Enable users to change spaces entered for indent tabs.
503 Changing indent_scale value with the mouse sets Var space_num,
504 which invokes the default callback to add an entry to
505 changes. Load_tab_cfg initializes space_num to default.
506
Cheryl Sabella2f896462017-08-14 21:21:43 -0400507 Widgets for FontPage(Frame): (*) widgets bound to self
508 frame_font: LabelFrame
509 frame_font_name: Frame
510 font_name_title: Label
511 (*)fontlist: ListBox - font_name
512 scroll_font: Scrollbar
513 frame_font_param: Frame
514 font_size_title: Label
515 (*)sizelist: DynOptionMenu - font_size
516 (*)bold_toggle: Checkbutton - font_bold
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400517 frame_sample: LabelFrame
518 (*)font_sample: Label
Cheryl Sabella2f896462017-08-14 21:21:43 -0400519 frame_indent: LabelFrame
520 indent_title: Label
521 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400522 """
csabella6f446be2017-08-01 00:24:07 -0400523 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
524 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
525 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400526 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
527
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400528 # Define frames and widgets.
csabella9397e2a2017-07-30 13:34:25 -0400529 frame_font = LabelFrame(
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400530 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
531 frame_sample = LabelFrame(
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300532 self, borderwidth=2, relief=GROOVE,
533 text=' Font Sample (Editable) ')
csabella9397e2a2017-07-30 13:34:25 -0400534 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400535 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400536 # frame_font.
537 frame_font_name = Frame(frame_font)
538 frame_font_param = Frame(frame_font)
539 font_name_title = Label(
540 frame_font_name, justify=LEFT, text='Font Face :')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400541 self.fontlist = Listbox(frame_font_name, height=15,
csabella9397e2a2017-07-30 13:34:25 -0400542 takefocus=True, exportselection=FALSE)
543 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
544 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
545 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
546 scroll_font = Scrollbar(frame_font_name)
547 scroll_font.config(command=self.fontlist.yview)
548 self.fontlist.config(yscrollcommand=scroll_font.set)
549 font_size_title = Label(frame_font_param, text='Size :')
550 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
551 self.bold_toggle = Checkbutton(
552 frame_font_param, variable=self.font_bold,
553 onvalue=1, offvalue=0, text='Bold')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400554 # frame_sample.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300555 self.font_sample = Text(frame_sample, width=20, height=20)
556 self.font_sample.insert(END, font_sample_text)
csabella9397e2a2017-07-30 13:34:25 -0400557 # frame_indent.
558 indent_title = Label(
559 frame_indent, justify=LEFT,
560 text='Python Standard: 4 Spaces!')
561 self.indent_scale = Scale(
562 frame_indent, variable=self.space_num,
563 orient='horizontal', tickinterval=2, from_=2, to=16)
564
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400565 # Grid and pack widgets:
566 self.columnconfigure(1, weight=1)
567 frame_font.grid(row=0, column=0, padx=5, pady=5)
568 frame_sample.grid(row=0, column=1, rowspan=2, padx=5, pady=5,
569 sticky='nsew')
570 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
csabella9397e2a2017-07-30 13:34:25 -0400571 # frame_font.
572 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
573 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
574 font_name_title.pack(side=TOP, anchor=W)
575 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
576 scroll_font.pack(side=LEFT, fill=Y)
577 font_size_title.pack(side=LEFT, anchor=W)
578 self.sizelist.pack(side=LEFT, anchor=W)
579 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400580 # frame_sample.
csabella9397e2a2017-07-30 13:34:25 -0400581 self.font_sample.pack(expand=TRUE, fill=BOTH)
582 # frame_indent.
csabella9397e2a2017-07-30 13:34:25 -0400583 indent_title.pack(side=TOP, anchor=W, padx=5)
584 self.indent_scale.pack(side=TOP, padx=5, fill=X)
585
csabella9397e2a2017-07-30 13:34:25 -0400586 def load_font_cfg(self):
587 """Load current configuration settings for the font options.
588
589 Retrieve current font with idleConf.GetFont and font families
590 from tk. Setup fontlist and set font_name. Setup sizelist,
591 which sets font_size. Set font_bold. Call set_samples.
592 """
593 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
594 font_name = configured_font[0].lower()
595 font_size = configured_font[1]
596 font_bold = configured_font[2]=='bold'
597
598 # Set editor font selection list and font_name.
599 fonts = list(tkFont.families(self))
600 fonts.sort()
601 for font in fonts:
602 self.fontlist.insert(END, font)
603 self.font_name.set(font_name)
604 lc_fonts = [s.lower() for s in fonts]
605 try:
606 current_font_index = lc_fonts.index(font_name)
607 self.fontlist.see(current_font_index)
608 self.fontlist.select_set(current_font_index)
609 self.fontlist.select_anchor(current_font_index)
610 self.fontlist.activate(current_font_index)
611 except ValueError:
612 pass
613 # Set font size dropdown.
614 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
615 '16', '18', '20', '22', '25', '29', '34', '40'),
616 font_size)
617 # Set font weight.
618 self.font_bold.set(font_bold)
619 self.set_samples()
620
621 def var_changed_font(self, *params):
622 """Store changes to font attributes.
623
624 When one font attribute changes, save them all, as they are
625 not independent from each other. In particular, when we are
626 overriding the default font, we need to write out everything.
627 """
628 value = self.font_name.get()
629 changes.add_option('main', 'EditorWindow', 'font', value)
630 value = self.font_size.get()
631 changes.add_option('main', 'EditorWindow', 'font-size', value)
632 value = self.font_bold.get()
633 changes.add_option('main', 'EditorWindow', 'font-bold', value)
634 self.set_samples()
635
636 def on_fontlist_select(self, event):
637 """Handle selecting a font from the list.
638
639 Event can result from either mouse click or Up or Down key.
640 Set font_name and example displays to selection.
641 """
642 font = self.fontlist.get(
643 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
644 self.font_name.set(font.lower())
645
646 def set_samples(self, event=None):
647 """Update update both screen samples with the font settings.
648
649 Called on font initialization and change events.
650 Accesses font_name, font_size, and font_bold Variables.
Leo Ariasc3d95082018-02-03 18:36:10 -0600651 Updates font_sample and highlight page highlight_sample.
csabella9397e2a2017-07-30 13:34:25 -0400652 """
653 font_name = self.font_name.get()
654 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
655 new_font = (font_name, self.font_size.get(), font_weight)
656 self.font_sample['font'] = new_font
657 self.highlight_sample['font'] = new_font
658
659 def load_tab_cfg(self):
660 """Load current configuration settings for the tab options.
661
662 Attributes updated:
663 space_num: Set to value from idleConf.
664 """
665 # Set indent sizes.
666 space_num = idleConf.GetOption(
667 'main', 'Indent', 'num-spaces', default=4, type='int')
668 self.space_num.set(space_num)
669
670 def var_changed_space_num(self, *params):
671 "Store change to indentation size."
672 value = self.space_num.get()
673 changes.add_option('main', 'Indent', 'num-spaces', value)
674
675
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400676class HighPage(Frame):
677
678 def __init__(self, master):
679 super().__init__(master)
680 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400681 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400682 self.create_page_highlight()
683 self.load_theme_cfg()
684
685 def create_page_highlight(self):
686 """Return frame of widgets for Highlighting tab.
687
688 Enable users to provisionally change foreground and background
689 colors applied to textual tags. Color mappings are stored in
690 complete listings called themes. Built-in themes in
691 idlelib/config-highlight.def are fixed as far as the dialog is
692 concerned. Any theme can be used as the base for a new custom
693 theme, stored in .idlerc/config-highlight.cfg.
694
695 Function load_theme_cfg() initializes tk variables and theme
696 lists and calls paint_theme_sample() and set_highlight_target()
697 for the current theme. Radiobuttons builtin_theme_on and
698 custom_theme_on toggle var theme_source, which controls if the
699 current set of colors are from a builtin or custom theme.
700 DynOptionMenus builtinlist and customlist contain lists of the
701 builtin and custom themes, respectively, and the current item
702 from each list is stored in vars builtin_name and custom_name.
703
704 Function paint_theme_sample() applies the colors from the theme
705 to the tags in text widget highlight_sample and then invokes
706 set_color_sample(). Function set_highlight_target() sets the state
707 of the radiobuttons fg_on and bg_on based on the tag and it also
708 invokes set_color_sample().
709
710 Function set_color_sample() sets the background color for the frame
711 holding the color selector. This provides a larger visual of the
712 color for the current tag and plane (foreground/background).
713
714 Note: set_color_sample() is called from many places and is often
715 called more than once when a change is made. It is invoked when
716 foreground or background is selected (radiobuttons), from
717 paint_theme_sample() (theme is changed or load_cfg is called), and
718 from set_highlight_target() (target tag is changed or load_cfg called).
719
720 Button delete_custom invokes delete_custom() to delete
721 a custom theme from idleConf.userCfg['highlight'] and changes.
722 Button save_custom invokes save_as_new_theme() which calls
723 get_new_theme_name() and create_new() to save a custom theme
724 and its colors to idleConf.userCfg['highlight'].
725
726 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
727 if the current selected color for a tag is for the foreground or
728 background.
729
730 DynOptionMenu targetlist contains a readable description of the
731 tags applied to Python source within IDLE. Selecting one of the
732 tags from this list populates highlight_target, which has a callback
733 function set_highlight_target().
734
735 Text widget highlight_sample displays a block of text (which is
736 mock Python code) in which is embedded the defined tags and reflects
737 the color attributes of the current theme and changes for those tags.
738 Mouse button 1 allows for selection of a tag and updates
739 highlight_target with that tag value.
740
741 Note: The font in highlight_sample is set through the config in
742 the fonts tab.
743
744 In other words, a tag can be selected either from targetlist or
745 by clicking on the sample text within highlight_sample. The
746 plane (foreground/background) is selected via the radiobutton.
747 Together, these two (tag and plane) control what color is
748 shown in set_color_sample() for the current theme. Button set_color
749 invokes get_color() which displays a ColorChooser to change the
750 color for the selected tag/plane. If a new color is picked,
751 it will be saved to changes and the highlight_sample and
752 frame background will be updated.
753
754 Tk Variables:
755 color: Color of selected target.
756 builtin_name: Menu variable for built-in theme.
757 custom_name: Menu variable for custom theme.
758 fg_bg_toggle: Toggle for foreground/background color.
759 Note: this has no callback.
760 theme_source: Selector for built-in or custom theme.
761 highlight_target: Menu variable for the highlight tag target.
762
763 Instance Data Attributes:
764 theme_elements: Dictionary of tags for text highlighting.
765 The key is the display name and the value is a tuple of
766 (tag name, display sort order).
767
768 Methods [attachment]:
769 load_theme_cfg: Load current highlight colors.
770 get_color: Invoke colorchooser [button_set_color].
771 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
772 set_highlight_target: set fg_bg_toggle, set_color_sample().
773 set_color_sample: Set frame background to target.
774 on_new_color_set: Set new color and add option.
775 paint_theme_sample: Recolor sample.
776 get_new_theme_name: Get from popup.
777 create_new: Combine theme with changes and save.
778 save_as_new_theme: Save [button_save_custom].
779 set_theme_type: Command for [theme_source].
780 delete_custom: Activate default [button_delete_custom].
781 save_new: Save to userCfg['theme'] (is function).
782
783 Widgets of highlights page frame: (*) widgets bound to self
784 frame_custom: LabelFrame
785 (*)highlight_sample: Text
786 (*)frame_color_set: Frame
787 (*)button_set_color: Button
788 (*)targetlist: DynOptionMenu - highlight_target
789 frame_fg_bg_toggle: Frame
790 (*)fg_on: Radiobutton - fg_bg_toggle
791 (*)bg_on: Radiobutton - fg_bg_toggle
792 (*)button_save_custom: Button
793 frame_theme: LabelFrame
794 theme_type_title: Label
795 (*)builtin_theme_on: Radiobutton - theme_source
796 (*)custom_theme_on: Radiobutton - theme_source
797 (*)builtinlist: DynOptionMenu - builtin_name
798 (*)customlist: DynOptionMenu - custom_name
799 (*)button_delete_custom: Button
800 (*)theme_message: Label
801 """
802 self.theme_elements = {
803 'Normal Text': ('normal', '00'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400804 'Code Context': ('context', '01'),
805 'Python Keywords': ('keyword', '02'),
806 'Python Definitions': ('definition', '03'),
807 'Python Builtins': ('builtin', '04'),
808 'Python Comments': ('comment', '05'),
809 'Python Strings': ('string', '06'),
810 'Selected Text': ('hilite', '07'),
811 'Found Text': ('hit', '08'),
812 'Cursor': ('cursor', '09'),
813 'Editor Breakpoint': ('break', '10'),
814 'Shell Normal Text': ('console', '11'),
815 'Shell Error Text': ('error', '12'),
816 'Shell Stdout Text': ('stdout', '13'),
817 'Shell Stderr Text': ('stderr', '14'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400818 }
819 self.builtin_name = tracers.add(
820 StringVar(self), self.var_changed_builtin_name)
821 self.custom_name = tracers.add(
822 StringVar(self), self.var_changed_custom_name)
823 self.fg_bg_toggle = BooleanVar(self)
824 self.color = tracers.add(
825 StringVar(self), self.var_changed_color)
826 self.theme_source = tracers.add(
827 BooleanVar(self), self.var_changed_theme_source)
828 self.highlight_target = tracers.add(
829 StringVar(self), self.var_changed_highlight_target)
830
831 # Create widgets:
832 # body frame and section frames.
833 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
834 text=' Custom Highlighting ')
835 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
836 text=' Highlighting Theme ')
837 # frame_custom.
838 text = self.highlight_sample = Text(
839 frame_custom, relief=SOLID, borderwidth=1,
840 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
841 takefocus=FALSE, highlightthickness=0, wrap=NONE)
842 text.bind('<Double-Button-1>', lambda e: 'break')
843 text.bind('<B1-Motion>', lambda e: 'break')
wohlganger58fc71c2017-09-10 16:19:47 -0500844 text_and_tags=(
845 ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400846 ('#you can click here', 'comment'), ('\n', 'normal'),
847 ('#to choose items', 'comment'), ('\n', 'normal'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400848 ('code context section', 'context'), ('\n\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400849 ('def', 'keyword'), (' ', 'normal'),
850 ('func', 'definition'), ('(param):\n ', 'normal'),
851 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
852 ("'string'", 'string'), ('\n var1 = ', 'normal'),
853 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
854 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
855 ('list', 'builtin'), ('(', 'normal'),
856 ('None', 'keyword'), (')\n', 'normal'),
857 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
858 (' error ', 'error'), (' ', 'normal'),
859 ('cursor |', 'cursor'), ('\n ', 'normal'),
860 ('shell', 'console'), (' ', 'normal'),
861 ('stdout', 'stdout'), (' ', 'normal'),
862 ('stderr', 'stderr'), ('\n\n', 'normal'))
863 for texttag in text_and_tags:
864 text.insert(END, texttag[0], texttag[1])
865 for element in self.theme_elements:
866 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400867 # event.widget.winfo_top_level().highlight_target.set(elem)
868 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400869 text.tag_bind(
870 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400871 text['state'] = 'disabled'
872 self.style.configure('frame_color_set.TFrame', borderwidth=1,
873 relief='solid')
874 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400875 frame_fg_bg_toggle = Frame(frame_custom)
876 self.button_set_color = Button(
877 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400878 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400879 self.targetlist = DynOptionMenu(
880 self.frame_color_set, self.highlight_target, None,
881 highlightthickness=0) #, command=self.set_highlight_targetBinding
882 self.fg_on = Radiobutton(
883 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
884 text='Foreground', command=self.set_color_sample_binding)
885 self.bg_on = Radiobutton(
886 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
887 text='Background', command=self.set_color_sample_binding)
888 self.fg_bg_toggle.set(1)
889 self.button_save_custom = Button(
890 frame_custom, text='Save as New Custom Theme',
891 command=self.save_as_new_theme)
892 # frame_theme.
893 theme_type_title = Label(frame_theme, text='Select : ')
894 self.builtin_theme_on = Radiobutton(
895 frame_theme, variable=self.theme_source, value=1,
896 command=self.set_theme_type, text='a Built-in Theme')
897 self.custom_theme_on = Radiobutton(
898 frame_theme, variable=self.theme_source, value=0,
899 command=self.set_theme_type, text='a Custom Theme')
900 self.builtinlist = DynOptionMenu(
901 frame_theme, self.builtin_name, None, command=None)
902 self.customlist = DynOptionMenu(
903 frame_theme, self.custom_name, None, command=None)
904 self.button_delete_custom = Button(
905 frame_theme, text='Delete Custom Theme',
906 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400907 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400908 # Pack widgets:
909 # body.
910 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500911 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400912 # frame_custom.
913 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
914 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
915 self.highlight_sample.pack(
916 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
917 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
918 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
919 self.fg_on.pack(side=LEFT, anchor=E)
920 self.bg_on.pack(side=RIGHT, anchor=W)
921 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
922 # frame_theme.
923 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
924 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
925 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
926 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
927 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
928 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
929 self.theme_message.pack(side=TOP, fill=X, pady=5)
930
931 def load_theme_cfg(self):
932 """Load current configuration settings for the theme options.
933
934 Based on the theme_source toggle, the theme is set as
935 either builtin or custom and the initial widget values
936 reflect the current settings from idleConf.
937
938 Attributes updated:
939 theme_source: Set from idleConf.
940 builtinlist: List of default themes from idleConf.
941 customlist: List of custom themes from idleConf.
942 custom_theme_on: Disabled if there are no custom themes.
943 custom_theme: Message with additional information.
944 targetlist: Create menu from self.theme_elements.
945
946 Methods:
947 set_theme_type
948 paint_theme_sample
949 set_highlight_target
950 """
951 # Set current theme type radiobutton.
952 self.theme_source.set(idleConf.GetOption(
953 'main', 'Theme', 'default', type='bool', default=1))
954 # Set current theme.
955 current_option = idleConf.CurrentTheme()
956 # Load available theme option menus.
957 if self.theme_source.get(): # Default theme selected.
958 item_list = idleConf.GetSectionList('default', 'highlight')
959 item_list.sort()
960 self.builtinlist.SetMenu(item_list, current_option)
961 item_list = idleConf.GetSectionList('user', 'highlight')
962 item_list.sort()
963 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400964 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400965 self.custom_name.set('- no custom themes -')
966 else:
967 self.customlist.SetMenu(item_list, item_list[0])
968 else: # User theme selected.
969 item_list = idleConf.GetSectionList('user', 'highlight')
970 item_list.sort()
971 self.customlist.SetMenu(item_list, current_option)
972 item_list = idleConf.GetSectionList('default', 'highlight')
973 item_list.sort()
974 self.builtinlist.SetMenu(item_list, item_list[0])
975 self.set_theme_type()
976 # Load theme element option menu.
977 theme_names = list(self.theme_elements.keys())
978 theme_names.sort(key=lambda x: self.theme_elements[x][1])
979 self.targetlist.SetMenu(theme_names, theme_names[0])
980 self.paint_theme_sample()
981 self.set_highlight_target()
982
983 def var_changed_builtin_name(self, *params):
984 """Process new builtin theme selection.
985
986 Add the changed theme's name to the changed_items and recreate
987 the sample with the values from the selected theme.
988 """
989 old_themes = ('IDLE Classic', 'IDLE New')
990 value = self.builtin_name.get()
991 if value not in old_themes:
992 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
993 changes.add_option('main', 'Theme', 'name', old_themes[0])
994 changes.add_option('main', 'Theme', 'name2', value)
995 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400996 else:
997 changes.add_option('main', 'Theme', 'name', value)
998 changes.add_option('main', 'Theme', 'name2', '')
999 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001000 self.paint_theme_sample()
1001
1002 def var_changed_custom_name(self, *params):
1003 """Process new custom theme selection.
1004
1005 If a new custom theme is selected, add the name to the
1006 changed_items and apply the theme to the sample.
1007 """
1008 value = self.custom_name.get()
1009 if value != '- no custom themes -':
1010 changes.add_option('main', 'Theme', 'name', value)
1011 self.paint_theme_sample()
1012
1013 def var_changed_theme_source(self, *params):
1014 """Process toggle between builtin and custom theme.
1015
1016 Update the default toggle value and apply the newly
1017 selected theme type.
1018 """
1019 value = self.theme_source.get()
1020 changes.add_option('main', 'Theme', 'default', value)
1021 if value:
1022 self.var_changed_builtin_name()
1023 else:
1024 self.var_changed_custom_name()
1025
1026 def var_changed_color(self, *params):
1027 "Process change to color choice."
1028 self.on_new_color_set()
1029
1030 def var_changed_highlight_target(self, *params):
1031 "Process selection of new target tag for highlighting."
1032 self.set_highlight_target()
1033
1034 def set_theme_type(self):
1035 """Set available screen options based on builtin or custom theme.
1036
1037 Attributes accessed:
1038 theme_source
1039
1040 Attributes updated:
1041 builtinlist
1042 customlist
1043 button_delete_custom
1044 custom_theme_on
1045
1046 Called from:
1047 handler for builtin_theme_on and custom_theme_on
1048 delete_custom
1049 create_new
1050 load_theme_cfg
1051 """
1052 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001053 self.builtinlist['state'] = 'normal'
1054 self.customlist['state'] = 'disabled'
1055 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001056 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001057 self.builtinlist['state'] = 'disabled'
1058 self.custom_theme_on.state(('!disabled',))
1059 self.customlist['state'] = 'normal'
1060 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001061
1062 def get_color(self):
1063 """Handle button to select a new color for the target tag.
1064
1065 If a new color is selected while using a builtin theme, a
1066 name must be supplied to create a custom theme.
1067
1068 Attributes accessed:
1069 highlight_target
1070 frame_color_set
1071 theme_source
1072
1073 Attributes updated:
1074 color
1075
1076 Methods:
1077 get_new_theme_name
1078 create_new
1079 """
1080 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001081 prev_color = self.style.lookup(self.frame_color_set['style'],
1082 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001083 rgbTuplet, color_string = tkColorChooser.askcolor(
1084 parent=self, title='Pick new color for : '+target,
1085 initialcolor=prev_color)
1086 if color_string and (color_string != prev_color):
1087 # User didn't cancel and they chose a new color.
1088 if self.theme_source.get(): # Current theme is a built-in.
1089 message = ('Your changes will be saved as a new Custom Theme. '
1090 'Enter a name for your new Custom Theme below.')
1091 new_theme = self.get_new_theme_name(message)
1092 if not new_theme: # User cancelled custom theme creation.
1093 return
1094 else: # Create new custom theme based on previously active theme.
1095 self.create_new(new_theme)
1096 self.color.set(color_string)
1097 else: # Current theme is user defined.
1098 self.color.set(color_string)
1099
1100 def on_new_color_set(self):
1101 "Display sample of new color selection on the dialog."
1102 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001103 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001104 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1105 sample_element = self.theme_elements[self.highlight_target.get()][0]
1106 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1107 theme = self.custom_name.get()
1108 theme_element = sample_element + '-' + plane
1109 changes.add_option('highlight', theme, theme_element, new_color)
1110
1111 def get_new_theme_name(self, message):
1112 "Return name of new theme from query popup."
1113 used_names = (idleConf.GetSectionList('user', 'highlight') +
1114 idleConf.GetSectionList('default', 'highlight'))
1115 new_theme = SectionName(
1116 self, 'New Custom Theme', message, used_names).result
1117 return new_theme
1118
1119 def save_as_new_theme(self):
1120 """Prompt for new theme name and create the theme.
1121
1122 Methods:
1123 get_new_theme_name
1124 create_new
1125 """
1126 new_theme_name = self.get_new_theme_name('New Theme Name:')
1127 if new_theme_name:
1128 self.create_new(new_theme_name)
1129
1130 def create_new(self, new_theme_name):
1131 """Create a new custom theme with the given name.
1132
1133 Create the new theme based on the previously active theme
1134 with the current changes applied. Once it is saved, then
1135 activate the new theme.
1136
1137 Attributes accessed:
1138 builtin_name
1139 custom_name
1140
1141 Attributes updated:
1142 customlist
1143 theme_source
1144
1145 Method:
1146 save_new
1147 set_theme_type
1148 """
1149 if self.theme_source.get():
1150 theme_type = 'default'
1151 theme_name = self.builtin_name.get()
1152 else:
1153 theme_type = 'user'
1154 theme_name = self.custom_name.get()
1155 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1156 # Apply any of the old theme's unsaved changes to the new theme.
1157 if theme_name in changes['highlight']:
1158 theme_changes = changes['highlight'][theme_name]
1159 for element in theme_changes:
1160 new_theme[element] = theme_changes[element]
1161 # Save the new theme.
1162 self.save_new(new_theme_name, new_theme)
1163 # Change GUI over to the new theme.
1164 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1165 custom_theme_list.sort()
1166 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1167 self.theme_source.set(0)
1168 self.set_theme_type()
1169
1170 def set_highlight_target(self):
1171 """Set fg/bg toggle and color based on highlight tag target.
1172
1173 Instance variables accessed:
1174 highlight_target
1175
1176 Attributes updated:
1177 fg_on
1178 bg_on
1179 fg_bg_toggle
1180
1181 Methods:
1182 set_color_sample
1183
1184 Called from:
1185 var_changed_highlight_target
1186 load_theme_cfg
1187 """
1188 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001189 self.fg_on.state(('disabled',))
1190 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001191 self.fg_bg_toggle.set(1)
1192 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001193 self.fg_on.state(('!disabled',))
1194 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001195 self.fg_bg_toggle.set(1)
1196 self.set_color_sample()
1197
1198 def set_color_sample_binding(self, *args):
1199 """Change color sample based on foreground/background toggle.
1200
1201 Methods:
1202 set_color_sample
1203 """
1204 self.set_color_sample()
1205
1206 def set_color_sample(self):
1207 """Set the color of the frame background to reflect the selected target.
1208
1209 Instance variables accessed:
1210 theme_elements
1211 highlight_target
1212 fg_bg_toggle
1213 highlight_sample
1214
1215 Attributes updated:
1216 frame_color_set
1217 """
1218 # Set the color sample area.
1219 tag = self.theme_elements[self.highlight_target.get()][0]
1220 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1221 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001222 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001223
1224 def paint_theme_sample(self):
1225 """Apply the theme colors to each element tag in the sample text.
1226
1227 Instance attributes accessed:
1228 theme_elements
1229 theme_source
1230 builtin_name
1231 custom_name
1232
1233 Attributes updated:
1234 highlight_sample: Set the tag elements to the theme.
1235
1236 Methods:
1237 set_color_sample
1238
1239 Called from:
1240 var_changed_builtin_name
1241 var_changed_custom_name
1242 load_theme_cfg
1243 """
1244 if self.theme_source.get(): # Default theme
1245 theme = self.builtin_name.get()
1246 else: # User theme
1247 theme = self.custom_name.get()
1248 for element_title in self.theme_elements:
1249 element = self.theme_elements[element_title][0]
1250 colors = idleConf.GetHighlight(theme, element)
1251 if element == 'cursor': # Cursor sample needs special painting.
1252 colors['background'] = idleConf.GetHighlight(
1253 theme, 'normal', fgBg='bg')
1254 # Handle any unsaved changes to this theme.
1255 if theme in changes['highlight']:
1256 theme_dict = changes['highlight'][theme]
1257 if element + '-foreground' in theme_dict:
1258 colors['foreground'] = theme_dict[element + '-foreground']
1259 if element + '-background' in theme_dict:
1260 colors['background'] = theme_dict[element + '-background']
1261 self.highlight_sample.tag_config(element, **colors)
1262 self.set_color_sample()
1263
1264 def save_new(self, theme_name, theme):
1265 """Save a newly created theme to idleConf.
1266
1267 theme_name - string, the name of the new theme
1268 theme - dictionary containing the new theme
1269 """
1270 if not idleConf.userCfg['highlight'].has_section(theme_name):
1271 idleConf.userCfg['highlight'].add_section(theme_name)
1272 for element in theme:
1273 value = theme[element]
1274 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1275
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001276 def askyesno(self, *args, **kwargs):
1277 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001278 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001279
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001280 def delete_custom(self):
1281 """Handle event to delete custom theme.
1282
1283 The current theme is deactivated and the default theme is
1284 activated. The custom theme is permanently removed from
1285 the config file.
1286
1287 Attributes accessed:
1288 custom_name
1289
1290 Attributes updated:
1291 custom_theme_on
1292 customlist
1293 theme_source
1294 builtin_name
1295
1296 Methods:
1297 deactivate_current_config
1298 save_all_changed_extensions
1299 activate_config_changes
1300 set_theme_type
1301 """
1302 theme_name = self.custom_name.get()
1303 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001304 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001305 'Delete Theme', delmsg % theme_name, parent=self):
1306 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001307 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001308 # Remove theme from changes, config, and file.
1309 changes.delete_section('highlight', theme_name)
1310 # Reload user theme list.
1311 item_list = idleConf.GetSectionList('user', 'highlight')
1312 item_list.sort()
1313 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001314 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001315 self.customlist.SetMenu(item_list, '- no custom themes -')
1316 else:
1317 self.customlist.SetMenu(item_list, item_list[0])
1318 # Revert to default theme.
1319 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1320 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1321 # User can't back out of these changes, they must be applied now.
1322 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001323 self.cd.save_all_changed_extensions()
1324 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001325 self.set_theme_type()
1326
1327
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001328class KeysPage(Frame):
1329
1330 def __init__(self, master):
1331 super().__init__(master)
1332 self.cd = master.master
1333 self.create_page_keys()
1334 self.load_key_cfg()
1335
1336 def create_page_keys(self):
1337 """Return frame of widgets for Keys tab.
1338
1339 Enable users to provisionally change both individual and sets of
1340 keybindings (shortcut keys). Except for features implemented as
1341 extensions, keybindings are stored in complete sets called
1342 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1343 as far as the dialog is concerned. Any keyset can be used as the
1344 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1345
1346 Function load_key_cfg() initializes tk variables and keyset
1347 lists and calls load_keys_list for the current keyset.
1348 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1349 keyset_source, which controls if the current set of keybindings
1350 are from a builtin or custom keyset. DynOptionMenus builtinlist
1351 and customlist contain lists of the builtin and custom keysets,
1352 respectively, and the current item from each list is stored in
1353 vars builtin_name and custom_name.
1354
1355 Button delete_custom_keys invokes delete_custom_keys() to delete
1356 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1357 save_custom_keys invokes save_as_new_key_set() which calls
1358 get_new_keys_name() and create_new_key_set() to save a custom keyset
1359 and its keybindings to idleConf.userCfg['keys'].
1360
1361 Listbox bindingslist contains all of the keybindings for the
1362 selected keyset. The keybindings are loaded in load_keys_list()
1363 and are pairs of (event, [keys]) where keys can be a list
1364 of one or more key combinations to bind to the same event.
1365 Mouse button 1 click invokes on_bindingslist_select(), which
1366 allows button_new_keys to be clicked.
1367
1368 So, an item is selected in listbindings, which activates
1369 button_new_keys, and clicking button_new_keys calls function
1370 get_new_keys(). Function get_new_keys() gets the key mappings from the
1371 current keyset for the binding event item that was selected. The
1372 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001373 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001374 to be entered for that binding event. If the keys aren't
1375 changed, nothing happens. If the keys are changed and the keyset
1376 is a builtin, function get_new_keys_name() will be called
1377 for input of a custom keyset name. If no name is given, then the
1378 change to the keybinding will abort and no updates will be made. If
1379 a custom name is entered in the prompt or if the current keyset was
1380 already custom (and thus didn't require a prompt), then
1381 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1382 with the change to the event binding. The item listing in bindingslist
1383 is updated with the new keys. Var keybinding is also set which invokes
1384 the callback function, var_changed_keybinding, to add the change to
1385 the 'keys' or 'extensions' changes tracker based on the binding type.
1386
1387 Tk Variables:
1388 keybinding: Action/key bindings.
1389
1390 Methods:
1391 load_keys_list: Reload active set.
1392 create_new_key_set: Combine active keyset and changes.
1393 set_keys_type: Command for keyset_source.
1394 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1395 deactivate_current_config: Remove keys bindings in editors.
1396
1397 Widgets for KeysPage(frame): (*) widgets bound to self
1398 frame_key_sets: LabelFrame
1399 frames[0]: Frame
1400 (*)builtin_keyset_on: Radiobutton - var keyset_source
1401 (*)custom_keyset_on: Radiobutton - var keyset_source
1402 (*)builtinlist: DynOptionMenu - var builtin_name,
1403 func keybinding_selected
1404 (*)customlist: DynOptionMenu - var custom_name,
1405 func keybinding_selected
1406 (*)keys_message: Label
1407 frames[1]: Frame
1408 (*)button_delete_custom_keys: Button - delete_custom_keys
1409 (*)button_save_custom_keys: Button - save_as_new_key_set
1410 frame_custom: LabelFrame
1411 frame_target: Frame
1412 target_title: Label
1413 scroll_target_y: Scrollbar
1414 scroll_target_x: Scrollbar
1415 (*)bindingslist: ListBox - on_bindingslist_select
1416 (*)button_new_keys: Button - get_new_keys & ..._name
1417 """
1418 self.builtin_name = tracers.add(
1419 StringVar(self), self.var_changed_builtin_name)
1420 self.custom_name = tracers.add(
1421 StringVar(self), self.var_changed_custom_name)
1422 self.keyset_source = tracers.add(
1423 BooleanVar(self), self.var_changed_keyset_source)
1424 self.keybinding = tracers.add(
1425 StringVar(self), self.var_changed_keybinding)
1426
1427 # Create widgets:
1428 # body and section frames.
1429 frame_custom = LabelFrame(
1430 self, borderwidth=2, relief=GROOVE,
1431 text=' Custom Key Bindings ')
1432 frame_key_sets = LabelFrame(
1433 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1434 # frame_custom.
1435 frame_target = Frame(frame_custom)
1436 target_title = Label(frame_target, text='Action - Key(s)')
1437 scroll_target_y = Scrollbar(frame_target)
1438 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1439 self.bindingslist = Listbox(
1440 frame_target, takefocus=FALSE, exportselection=FALSE)
1441 self.bindingslist.bind('<ButtonRelease-1>',
1442 self.on_bindingslist_select)
1443 scroll_target_y['command'] = self.bindingslist.yview
1444 scroll_target_x['command'] = self.bindingslist.xview
1445 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1446 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1447 self.button_new_keys = Button(
1448 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001449 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001450 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001451 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001452 for i in range(2)]
1453 self.builtin_keyset_on = Radiobutton(
1454 frames[0], variable=self.keyset_source, value=1,
1455 command=self.set_keys_type, text='Use a Built-in Key Set')
1456 self.custom_keyset_on = Radiobutton(
1457 frames[0], variable=self.keyset_source, value=0,
1458 command=self.set_keys_type, text='Use a Custom Key Set')
1459 self.builtinlist = DynOptionMenu(
1460 frames[0], self.builtin_name, None, command=None)
1461 self.customlist = DynOptionMenu(
1462 frames[0], self.custom_name, None, command=None)
1463 self.button_delete_custom_keys = Button(
1464 frames[1], text='Delete Custom Key Set',
1465 command=self.delete_custom_keys)
1466 self.button_save_custom_keys = Button(
1467 frames[1], text='Save as New Custom Key Set',
1468 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001469 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001470
1471 # Pack widgets:
1472 # body.
1473 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1474 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1475 # frame_custom.
1476 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1477 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1478 # frame_target.
1479 frame_target.columnconfigure(0, weight=1)
1480 frame_target.rowconfigure(1, weight=1)
1481 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1482 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1483 scroll_target_y.grid(row=1, column=1, sticky=NS)
1484 scroll_target_x.grid(row=2, column=0, sticky=EW)
1485 # frame_key_sets.
1486 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1487 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1488 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1489 self.customlist.grid(row=1, column=1, sticky=NSEW)
1490 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1491 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1492 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1493 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1494 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1495
1496 def load_key_cfg(self):
1497 "Load current configuration settings for the keybinding options."
1498 # Set current keys type radiobutton.
1499 self.keyset_source.set(idleConf.GetOption(
1500 'main', 'Keys', 'default', type='bool', default=1))
1501 # Set current keys.
1502 current_option = idleConf.CurrentKeys()
1503 # Load available keyset option menus.
1504 if self.keyset_source.get(): # Default theme selected.
1505 item_list = idleConf.GetSectionList('default', 'keys')
1506 item_list.sort()
1507 self.builtinlist.SetMenu(item_list, current_option)
1508 item_list = idleConf.GetSectionList('user', 'keys')
1509 item_list.sort()
1510 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001511 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001512 self.custom_name.set('- no custom keys -')
1513 else:
1514 self.customlist.SetMenu(item_list, item_list[0])
1515 else: # User key set selected.
1516 item_list = idleConf.GetSectionList('user', 'keys')
1517 item_list.sort()
1518 self.customlist.SetMenu(item_list, current_option)
1519 item_list = idleConf.GetSectionList('default', 'keys')
1520 item_list.sort()
1521 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1522 self.set_keys_type()
1523 # Load keyset element list.
1524 keyset_name = idleConf.CurrentKeys()
1525 self.load_keys_list(keyset_name)
1526
1527 def var_changed_builtin_name(self, *params):
1528 "Process selection of builtin key set."
1529 old_keys = (
1530 'IDLE Classic Windows',
1531 'IDLE Classic Unix',
1532 'IDLE Classic Mac',
1533 'IDLE Classic OSX',
1534 )
1535 value = self.builtin_name.get()
1536 if value not in old_keys:
1537 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1538 changes.add_option('main', 'Keys', 'name', old_keys[0])
1539 changes.add_option('main', 'Keys', 'name2', value)
1540 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001541 else:
1542 changes.add_option('main', 'Keys', 'name', value)
1543 changes.add_option('main', 'Keys', 'name2', '')
1544 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001545 self.load_keys_list(value)
1546
1547 def var_changed_custom_name(self, *params):
1548 "Process selection of custom key set."
1549 value = self.custom_name.get()
1550 if value != '- no custom keys -':
1551 changes.add_option('main', 'Keys', 'name', value)
1552 self.load_keys_list(value)
1553
1554 def var_changed_keyset_source(self, *params):
1555 "Process toggle between builtin key set and custom key set."
1556 value = self.keyset_source.get()
1557 changes.add_option('main', 'Keys', 'default', value)
1558 if value:
1559 self.var_changed_builtin_name()
1560 else:
1561 self.var_changed_custom_name()
1562
1563 def var_changed_keybinding(self, *params):
1564 "Store change to a keybinding."
1565 value = self.keybinding.get()
1566 key_set = self.custom_name.get()
1567 event = self.bindingslist.get(ANCHOR).split()[0]
1568 if idleConf.IsCoreBinding(event):
1569 changes.add_option('keys', key_set, event, value)
1570 else: # Event is an extension binding.
1571 ext_name = idleConf.GetExtnNameForEvent(event)
1572 ext_keybind_section = ext_name + '_cfgBindings'
1573 changes.add_option('extensions', ext_keybind_section, event, value)
1574
1575 def set_keys_type(self):
1576 "Set available screen options based on builtin or custom key set."
1577 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001578 self.builtinlist['state'] = 'normal'
1579 self.customlist['state'] = 'disabled'
1580 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001581 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001582 self.builtinlist['state'] = 'disabled'
1583 self.custom_keyset_on.state(('!disabled',))
1584 self.customlist['state'] = 'normal'
1585 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001586
1587 def get_new_keys(self):
1588 """Handle event to change key binding for selected line.
1589
1590 A selection of a key/binding in the list of current
1591 bindings pops up a dialog to enter a new binding. If
1592 the current key set is builtin and a binding has
1593 changed, then a name for a custom key set needs to be
1594 entered for the change to be applied.
1595 """
1596 list_index = self.bindingslist.index(ANCHOR)
1597 binding = self.bindingslist.get(list_index)
1598 bind_name = binding.split()[0]
1599 if self.keyset_source.get():
1600 current_key_set_name = self.builtin_name.get()
1601 else:
1602 current_key_set_name = self.custom_name.get()
1603 current_bindings = idleConf.GetCurrentKeySet()
1604 if current_key_set_name in changes['keys']: # unsaved changes
1605 key_set_changes = changes['keys'][current_key_set_name]
1606 for event in key_set_changes:
1607 current_bindings[event] = key_set_changes[event].split()
1608 current_key_sequences = list(current_bindings.values())
1609 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1610 current_key_sequences).result
1611 if new_keys:
1612 if self.keyset_source.get(): # Current key set is a built-in.
1613 message = ('Your changes will be saved as a new Custom Key Set.'
1614 ' Enter a name for your new Custom Key Set below.')
1615 new_keyset = self.get_new_keys_name(message)
1616 if not new_keyset: # User cancelled custom key set creation.
1617 self.bindingslist.select_set(list_index)
1618 self.bindingslist.select_anchor(list_index)
1619 return
1620 else: # Create new custom key set based on previously active key set.
1621 self.create_new_key_set(new_keyset)
1622 self.bindingslist.delete(list_index)
1623 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1624 self.bindingslist.select_set(list_index)
1625 self.bindingslist.select_anchor(list_index)
1626 self.keybinding.set(new_keys)
1627 else:
1628 self.bindingslist.select_set(list_index)
1629 self.bindingslist.select_anchor(list_index)
1630
1631 def get_new_keys_name(self, message):
1632 "Return new key set name from query popup."
1633 used_names = (idleConf.GetSectionList('user', 'keys') +
1634 idleConf.GetSectionList('default', 'keys'))
1635 new_keyset = SectionName(
1636 self, 'New Custom Key Set', message, used_names).result
1637 return new_keyset
1638
1639 def save_as_new_key_set(self):
1640 "Prompt for name of new key set and save changes using that name."
1641 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1642 if new_keys_name:
1643 self.create_new_key_set(new_keys_name)
1644
1645 def on_bindingslist_select(self, event):
1646 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001647 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001648
1649 def create_new_key_set(self, new_key_set_name):
1650 """Create a new custom key set with the given name.
1651
1652 Copy the bindings/keys from the previously active keyset
1653 to the new keyset and activate the new custom keyset.
1654 """
1655 if self.keyset_source.get():
1656 prev_key_set_name = self.builtin_name.get()
1657 else:
1658 prev_key_set_name = self.custom_name.get()
1659 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1660 new_keys = {}
1661 for event in prev_keys: # Add key set to changed items.
1662 event_name = event[2:-2] # Trim off the angle brackets.
1663 binding = ' '.join(prev_keys[event])
1664 new_keys[event_name] = binding
1665 # Handle any unsaved changes to prev key set.
1666 if prev_key_set_name in changes['keys']:
1667 key_set_changes = changes['keys'][prev_key_set_name]
1668 for event in key_set_changes:
1669 new_keys[event] = key_set_changes[event]
1670 # Save the new key set.
1671 self.save_new_key_set(new_key_set_name, new_keys)
1672 # Change GUI over to the new key set.
1673 custom_key_list = idleConf.GetSectionList('user', 'keys')
1674 custom_key_list.sort()
1675 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1676 self.keyset_source.set(0)
1677 self.set_keys_type()
1678
1679 def load_keys_list(self, keyset_name):
1680 """Reload the list of action/key binding pairs for the active key set.
1681
1682 An action/key binding can be selected to change the key binding.
1683 """
1684 reselect = False
1685 if self.bindingslist.curselection():
1686 reselect = True
1687 list_index = self.bindingslist.index(ANCHOR)
1688 keyset = idleConf.GetKeySet(keyset_name)
1689 bind_names = list(keyset.keys())
1690 bind_names.sort()
1691 self.bindingslist.delete(0, END)
1692 for bind_name in bind_names:
1693 key = ' '.join(keyset[bind_name])
1694 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1695 if keyset_name in changes['keys']:
1696 # Handle any unsaved changes to this key set.
1697 if bind_name in changes['keys'][keyset_name]:
1698 key = changes['keys'][keyset_name][bind_name]
1699 self.bindingslist.insert(END, bind_name+' - '+key)
1700 if reselect:
1701 self.bindingslist.see(list_index)
1702 self.bindingslist.select_set(list_index)
1703 self.bindingslist.select_anchor(list_index)
1704
1705 @staticmethod
1706 def save_new_key_set(keyset_name, keyset):
1707 """Save a newly created core key set.
1708
1709 Add keyset to idleConf.userCfg['keys'], not to disk.
1710 If the keyset doesn't exist, it is created. The
1711 binding/keys are taken from the keyset argument.
1712
1713 keyset_name - string, the name of the new key set
1714 keyset - dictionary containing the new keybindings
1715 """
1716 if not idleConf.userCfg['keys'].has_section(keyset_name):
1717 idleConf.userCfg['keys'].add_section(keyset_name)
1718 for event in keyset:
1719 value = keyset[event]
1720 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1721
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001722 def askyesno(self, *args, **kwargs):
1723 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001724 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001725
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001726 def delete_custom_keys(self):
1727 """Handle event to delete a custom key set.
1728
1729 Applying the delete deactivates the current configuration and
1730 reverts to the default. The custom key set is permanently
1731 deleted from the config file.
1732 """
1733 keyset_name = self.custom_name.get()
1734 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001735 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001736 'Delete Key Set', delmsg % keyset_name, parent=self):
1737 return
1738 self.cd.deactivate_current_config()
1739 # Remove key set from changes, config, and file.
1740 changes.delete_section('keys', keyset_name)
1741 # Reload user key set list.
1742 item_list = idleConf.GetSectionList('user', 'keys')
1743 item_list.sort()
1744 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001745 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001746 self.customlist.SetMenu(item_list, '- no custom keys -')
1747 else:
1748 self.customlist.SetMenu(item_list, item_list[0])
1749 # Revert to default key set.
1750 self.keyset_source.set(idleConf.defaultCfg['main']
1751 .Get('Keys', 'default'))
1752 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1753 or idleConf.default_keys())
1754 # User can't back out of these changes, they must be applied now.
1755 changes.save_all()
1756 self.cd.save_all_changed_extensions()
1757 self.cd.activate_config_changes()
1758 self.set_keys_type()
1759
1760
csabellae8eb17b2017-07-30 18:39:17 -04001761class GenPage(Frame):
1762
csabella6f446be2017-08-01 00:24:07 -04001763 def __init__(self, master):
1764 super().__init__(master)
csabellae8eb17b2017-07-30 18:39:17 -04001765 self.create_page_general()
1766 self.load_general_cfg()
1767
1768 def create_page_general(self):
1769 """Return frame of widgets for General tab.
1770
1771 Enable users to provisionally change general options. Function
1772 load_general_cfg intializes tk variables and helplist using
1773 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1774 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1775 set var autosave. Entry boxes win_width_int and win_height_int
1776 set var win_width and win_height. Setting var_name invokes the
1777 default callback that adds option to changes.
1778
1779 Helplist: load_general_cfg loads list user_helplist with
1780 name, position pairs and copies names to listbox helplist.
1781 Clicking a name invokes help_source selected. Clicking
1782 button_helplist_name invokes helplist_item_name, which also
1783 changes user_helplist. These functions all call
1784 set_add_delete_state. All but load call update_help_changes to
1785 rewrite changes['main']['HelpFiles'].
1786
Cheryl Sabella2f896462017-08-14 21:21:43 -04001787 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001788 frame_window: LabelFrame
1789 frame_run: Frame
1790 startup_title: Label
1791 (*)startup_editor_on: Radiobutton - startup_edit
1792 (*)startup_shell_on: Radiobutton - startup_edit
1793 frame_win_size: Frame
1794 win_size_title: Label
1795 win_width_title: Label
1796 (*)win_width_int: Entry - win_width
1797 win_height_title: Label
1798 (*)win_height_int: Entry - win_height
Cheryl Sabella845d8642018-02-04 18:15:21 -05001799 frame_autocomplete: Frame
1800 auto_wait_title: Label
1801 (*)auto_wait_int: Entry - autocomplete_wait
1802 frame_paren1: Frame
1803 paren_style_title: Label
1804 (*)paren_style_type: OptionMenu - paren_style
1805 frame_paren2: Frame
1806 paren_time_title: Label
1807 (*)paren_flash_time: Entry - flash_delay
1808 (*)bell_on: Checkbutton - paren_bell
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001809 frame_editor: LabelFrame
1810 frame_save: Frame
1811 run_save_title: Label
1812 (*)save_ask_on: Radiobutton - autosave
1813 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella845d8642018-02-04 18:15:21 -05001814 frame_format: Frame
1815 format_width_title: Label
1816 (*)format_width_int: Entry - format_width
1817 frame_context: Frame
1818 context_title: Label
1819 (*)context_int: Entry - context_lines
Cheryl Sabella2f896462017-08-14 21:21:43 -04001820 frame_help: LabelFrame
1821 frame_helplist: Frame
1822 frame_helplist_buttons: Frame
1823 (*)button_helplist_edit
1824 (*)button_helplist_add
1825 (*)button_helplist_remove
1826 (*)helplist: ListBox
1827 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001828 """
wohlganger58fc71c2017-09-10 16:19:47 -05001829 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001830 self.startup_edit = tracers.add(
1831 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001832 self.win_width = tracers.add(
1833 StringVar(self), ('main', 'EditorWindow', 'width'))
1834 self.win_height = tracers.add(
1835 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001836 self.autocomplete_wait = tracers.add(
1837 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1838 self.paren_style = tracers.add(
1839 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1840 self.flash_delay = tracers.add(
1841 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1842 self.paren_bell = tracers.add(
1843 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001844
wohlganger58fc71c2017-09-10 16:19:47 -05001845 self.autosave = tracers.add(
1846 IntVar(self), ('main', 'General', 'autosave'))
1847 self.format_width = tracers.add(
1848 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
1849 self.context_lines = tracers.add(
Cheryl Sabella29996a12018-06-01 19:23:00 -04001850 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
wohlganger58fc71c2017-09-10 16:19:47 -05001851
1852 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001853 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001854 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1855 text=' Window Preferences')
1856 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1857 text=' Editor Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001858 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1859 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001860 # Frame_window.
1861 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001862 startup_title = Label(frame_run, text='At Startup')
1863 self.startup_editor_on = Radiobutton(
1864 frame_run, variable=self.startup_edit, value=1,
1865 text="Open Edit Window")
1866 self.startup_shell_on = Radiobutton(
1867 frame_run, variable=self.startup_edit, value=0,
1868 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001869
wohlganger58fc71c2017-09-10 16:19:47 -05001870 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001871 win_size_title = Label(
1872 frame_win_size, text='Initial Window Size (in characters)')
1873 win_width_title = Label(frame_win_size, text='Width')
1874 self.win_width_int = Entry(
1875 frame_win_size, textvariable=self.win_width, width=3)
1876 win_height_title = Label(frame_win_size, text='Height')
1877 self.win_height_int = Entry(
1878 frame_win_size, textvariable=self.win_height, width=3)
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001879
wohlganger58fc71c2017-09-10 16:19:47 -05001880 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1881 auto_wait_title = Label(frame_autocomplete,
1882 text='Completions Popup Wait (milliseconds)')
1883 self.auto_wait_int = Entry(frame_autocomplete, width=6,
1884 textvariable=self.autocomplete_wait)
1885
1886 frame_paren1 = Frame(frame_window, borderwidth=0)
1887 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1888 self.paren_style_type = OptionMenu(
1889 frame_paren1, self.paren_style, 'expression',
1890 "opener","parens","expression")
1891 frame_paren2 = Frame(frame_window, borderwidth=0)
1892 paren_time_title = Label(
1893 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1894 '(0 is until next input)')
1895 self.paren_flash_time = Entry(
1896 frame_paren2, textvariable=self.flash_delay, width=6)
1897 self.bell_on = Checkbutton(
1898 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1899
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001900 # Frame_editor.
1901 frame_save = Frame(frame_editor, borderwidth=0)
1902 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1903 self.save_ask_on = Radiobutton(
1904 frame_save, variable=self.autosave, value=0,
1905 text="Prompt to Save")
1906 self.save_auto_on = Radiobutton(
1907 frame_save, variable=self.autosave, value=1,
1908 text='No Prompt')
1909
wohlganger58fc71c2017-09-10 16:19:47 -05001910 frame_format = Frame(frame_editor, borderwidth=0)
1911 format_width_title = Label(frame_format,
1912 text='Format Paragraph Max Width')
1913 self.format_width_int = Entry(
1914 frame_format, textvariable=self.format_width, width=4)
1915
1916 frame_context = Frame(frame_editor, borderwidth=0)
Cheryl Sabella29996a12018-06-01 19:23:00 -04001917 context_title = Label(frame_context, text='Max Context Lines :')
wohlganger58fc71c2017-09-10 16:19:47 -05001918 self.context_int = Entry(
1919 frame_context, textvariable=self.context_lines, width=3)
1920
1921
csabellae8eb17b2017-07-30 18:39:17 -04001922 # frame_help.
1923 frame_helplist = Frame(frame_help)
1924 frame_helplist_buttons = Frame(frame_helplist)
1925 self.helplist = Listbox(
1926 frame_helplist, height=5, takefocus=True,
1927 exportselection=FALSE)
1928 scroll_helplist = Scrollbar(frame_helplist)
1929 scroll_helplist['command'] = self.helplist.yview
1930 self.helplist['yscrollcommand'] = scroll_helplist.set
1931 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1932 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001933 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001934 width=8, command=self.helplist_item_edit)
1935 self.button_helplist_add = Button(
1936 frame_helplist_buttons, text='Add',
1937 width=8, command=self.helplist_item_add)
1938 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001939 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001940 width=8, command=self.helplist_item_remove)
1941
1942 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001943 # Body.
1944 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1945 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04001946 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1947 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001948 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001949 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1950 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1951 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04001952 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001953 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001954 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1955 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1956 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1957 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1958 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001959 # frame_autocomplete.
1960 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
1961 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1962 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
1963 # frame_paren.
1964 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
1965 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1966 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
1967 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
1968 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
1969 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1970 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
1971
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001972 # frame_save.
1973 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
1974 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1975 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1976 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001977 # frame_format.
1978 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
1979 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1980 self.format_width_int.pack(side=TOP, padx=10, pady=5)
1981 # frame_context.
1982 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
1983 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1984 self.context_int.pack(side=TOP, padx=5, pady=5)
1985
csabellae8eb17b2017-07-30 18:39:17 -04001986 # frame_help.
1987 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1988 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1989 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1990 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1991 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1992 self.button_helplist_add.pack(side=TOP, anchor=W)
1993 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1994
1995 def load_general_cfg(self):
1996 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05001997 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04001998 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05001999 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04002000 self.win_width.set(idleConf.GetOption(
2001 'main', 'EditorWindow', 'width', type='int'))
2002 self.win_height.set(idleConf.GetOption(
2003 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002004 self.autocomplete_wait.set(idleConf.GetOption(
2005 'extensions', 'AutoComplete', 'popupwait', type='int'))
2006 self.paren_style.set(idleConf.GetOption(
2007 'extensions', 'ParenMatch', 'style'))
2008 self.flash_delay.set(idleConf.GetOption(
2009 'extensions', 'ParenMatch', 'flash-delay', type='int'))
2010 self.paren_bell.set(idleConf.GetOption(
2011 'extensions', 'ParenMatch', 'bell'))
2012
2013 # Set variables for editor windows.
2014 self.autosave.set(idleConf.GetOption(
2015 'main', 'General', 'autosave', default=0, type='bool'))
2016 self.format_width.set(idleConf.GetOption(
2017 'extensions', 'FormatParagraph', 'max-width', type='int'))
2018 self.context_lines.set(idleConf.GetOption(
Cheryl Sabella29996a12018-06-01 19:23:00 -04002019 'extensions', 'CodeContext', 'maxlines', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002020
csabellae8eb17b2017-07-30 18:39:17 -04002021 # Set additional help sources.
2022 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2023 self.helplist.delete(0, 'end')
2024 for help_item in self.user_helplist:
2025 self.helplist.insert(END, help_item[0])
2026 self.set_add_delete_state()
2027
2028 def help_source_selected(self, event):
2029 "Handle event for selecting additional help."
2030 self.set_add_delete_state()
2031
2032 def set_add_delete_state(self):
2033 "Toggle the state for the help list buttons based on list entries."
2034 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002035 self.button_helplist_edit.state(('disabled',))
2036 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002037 else: # Some entries.
2038 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002039 self.button_helplist_edit.state(('!disabled',))
2040 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002041 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002042 self.button_helplist_edit.state(('disabled',))
2043 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002044
2045 def helplist_item_add(self):
2046 """Handle add button for the help list.
2047
2048 Query for name and location of new help sources and add
2049 them to the list.
2050 """
2051 help_source = HelpSource(self, 'New Help Source').result
2052 if help_source:
2053 self.user_helplist.append(help_source)
2054 self.helplist.insert(END, help_source[0])
2055 self.update_help_changes()
2056
2057 def helplist_item_edit(self):
2058 """Handle edit button for the help list.
2059
2060 Query with existing help source information and update
2061 config if the values are changed.
2062 """
2063 item_index = self.helplist.index(ANCHOR)
2064 help_source = self.user_helplist[item_index]
2065 new_help_source = HelpSource(
2066 self, 'Edit Help Source',
2067 menuitem=help_source[0],
2068 filepath=help_source[1],
2069 ).result
2070 if new_help_source and new_help_source != help_source:
2071 self.user_helplist[item_index] = new_help_source
2072 self.helplist.delete(item_index)
2073 self.helplist.insert(item_index, new_help_source[0])
2074 self.update_help_changes()
2075 self.set_add_delete_state() # Selected will be un-selected
2076
2077 def helplist_item_remove(self):
2078 """Handle remove button for the help list.
2079
2080 Delete the help list item from config.
2081 """
2082 item_index = self.helplist.index(ANCHOR)
2083 del(self.user_helplist[item_index])
2084 self.helplist.delete(item_index)
2085 self.update_help_changes()
2086 self.set_add_delete_state()
2087
2088 def update_help_changes(self):
2089 "Clear and rebuild the HelpFiles section in changes"
2090 changes['main']['HelpFiles'] = {}
2091 for num in range(1, len(self.user_helplist) + 1):
2092 changes.add_option(
2093 'main', 'HelpFiles', str(num),
2094 ';'.join(self.user_helplist[num-1][:2]))
2095
2096
csabella45bf7232017-07-26 19:09:58 -04002097class VarTrace:
2098 """Maintain Tk variables trace state."""
2099
2100 def __init__(self):
2101 """Store Tk variables and callbacks.
2102
2103 untraced: List of tuples (var, callback)
2104 that do not have the callback attached
2105 to the Tk var.
2106 traced: List of tuples (var, callback) where
2107 that callback has been attached to the var.
2108 """
2109 self.untraced = []
2110 self.traced = []
2111
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002112 def clear(self):
2113 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002114 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002115 self.untraced.clear()
2116 self.traced.clear()
2117
csabella45bf7232017-07-26 19:09:58 -04002118 def add(self, var, callback):
2119 """Add (var, callback) tuple to untraced list.
2120
2121 Args:
2122 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002123 callback: Either function name to be used as a callback
2124 or a tuple with IdleConf config-type, section, and
2125 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002126
2127 Return:
2128 Tk variable instance.
2129 """
2130 if isinstance(callback, tuple):
2131 callback = self.make_callback(var, callback)
2132 self.untraced.append((var, callback))
2133 return var
2134
2135 @staticmethod
2136 def make_callback(var, config):
2137 "Return default callback function to add values to changes instance."
2138 def default_callback(*params):
2139 "Add config values to changes instance."
2140 changes.add_option(*config, var.get())
2141 return default_callback
2142
2143 def attach(self):
2144 "Attach callback to all vars that are not traced."
2145 while self.untraced:
2146 var, callback = self.untraced.pop()
2147 var.trace_add('write', callback)
2148 self.traced.append((var, callback))
2149
2150 def detach(self):
2151 "Remove callback from traced vars."
2152 while self.traced:
2153 var, callback = self.traced.pop()
2154 var.trace_remove('write', var.trace_info()[0][1])
2155 self.untraced.append((var, callback))
2156
2157
csabella5b591542017-07-28 14:40:59 -04002158tracers = VarTrace()
2159
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002160help_common = '''\
2161When you click either the Apply or Ok buttons, settings in this
2162dialog that are different from IDLE's default are saved in
2163a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002164these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002165machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002166'''
2167help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002168 'Fonts/Tabs':'''
2169Font sample: This shows what a selection of Basic Multilingual Plane
2170unicode characters look like for the current font selection. If the
2171selected font does not define a character, Tk attempts to find another
2172font that does. Substitute glyphs depend on what is available on a
2173particular system and will not necessarily have the same size as the
2174font selected. Line contains 20 characters up to Devanagari, 14 for
2175Tamil, and 10 for East Asia.
2176
2177Hebrew and Arabic letters should display right to left, starting with
2178alef, \u05d0 and \u0627. Arabic digits display left to right. The
2179Devanagari and Tamil lines start with digits. The East Asian lines
2180are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2181Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002182
2183You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002184''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002185 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002186Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002187The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002188be used with older IDLE releases if it is saved as a custom
2189theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002190''',
2191 'Keys': '''
2192Keys:
2193The IDLE Modern Unix key set is new in June 2016. It can only
2194be used with older IDLE releases if it is saved as a custom
2195key set, with a different name.
2196''',
wohlganger58fc71c2017-09-10 16:19:47 -05002197 'General': '''
2198General:
wohlgangerfae2c352017-06-27 21:36:23 -05002199
wohlganger58fc71c2017-09-10 16:19:47 -05002200AutoComplete: Popupwait is milleseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002201cursor movement, before popping up completion box. Key char is '.' after
2202identifier or a '/' (or '\\' on Windows) within a string.
2203
2204FormatParagraph: Max-width is max chars in lines after re-formatting.
2205Use with paragraphs in both strings and comment blocks.
2206
2207ParenMatch: Style indicates what is highlighted when closer is entered:
2208'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2209'expression' (default) - also everything in between. Flash-delay is how
2210long to highlight if cursor is not moved (0 means forever).
Cheryl Sabella29996a12018-06-01 19:23:00 -04002211
2212CodeContext: Maxlines is the maximum number of code context lines to
2213display when Code Context is turned on for an editor window.
wohlgangerfae2c352017-06-27 21:36:23 -05002214'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002215}
2216
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002217
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002218def is_int(s):
2219 "Return 's is blank or represents an int'"
2220 if not s:
2221 return True
2222 try:
2223 int(s)
2224 return True
2225 except ValueError:
2226 return False
2227
2228
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002229class VerticalScrolledFrame(Frame):
2230 """A pure Tkinter vertically scrollable frame.
2231
2232 * Use the 'interior' attribute to place widgets inside the scrollable frame
2233 * Construct and pack/place/grid normally
2234 * This frame only allows vertical scrolling
2235 """
2236 def __init__(self, parent, *args, **kw):
2237 Frame.__init__(self, parent, *args, **kw)
2238
csabella7eb58832017-07-04 21:30:58 -04002239 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002240 vscrollbar = Scrollbar(self, orient=VERTICAL)
2241 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002242 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002243 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002244 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2245 vscrollbar.config(command=canvas.yview)
2246
csabella7eb58832017-07-04 21:30:58 -04002247 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002248 canvas.xview_moveto(0)
2249 canvas.yview_moveto(0)
2250
csabella7eb58832017-07-04 21:30:58 -04002251 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002252 self.interior = interior = Frame(canvas)
2253 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2254
csabella7eb58832017-07-04 21:30:58 -04002255 # Track changes to the canvas and frame width and sync them,
2256 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002257 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002258 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002259 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2260 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002261 interior.bind('<Configure>', _configure_interior)
2262
2263 def _configure_canvas(event):
2264 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002265 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002266 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2267 canvas.bind('<Configure>', _configure_canvas)
2268
2269 return
2270
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002271
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002272if __name__ == '__main__':
Terry Jan Reedy4d921582018-06-19 19:12:52 -04002273 from unittest import main
2274 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
2275
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002276 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002277 run(ConfigDialog)